Club Delphi  
    FTP   CCD     Buscar   Trucos   Trabajo   Foros

Retroceder   Foros Club Delphi > Principal > API de Windows
Registrarse FAQ Miembros Calendario Guía de estilo Buscar Temas de Hoy Marcar Foros Como Leídos

Respuesta
 
Herramientas Buscar en Tema Desplegado
  #1  
Antiguo 01-05-2008
rolandoj rolandoj is offline
Miembro
 
Registrado: abr 2007
Posts: 395
Poder: 17
rolandoj Va por buen camino
Smile Como inyectar código directamente a un proceso externo

Hola,

Ante una de petición de Khronos, he creído util crear un hilo para analizar el método de inyección de código dentro de un proceso directamente desde nuestro programa, como contraposición al método de inyectarlo vía un DLL.

La idea surge por un problema que me he encontrado y describo en este hilo :

http://www.clubdelphi.com/foros/showthread.php?t=55785

Voy a describir el proceso tal y como yo lo entiendo, advirtiendo que hay puntos en los que tengo dudas, o pudiera estar errado, así que espero que haya quienes puedan aclararlas.

Empiezo diciendo que este método tiene fuertes restricciones; pero es muy eficiente. Su uso solo se justifica cuando necesitamos acceder a alguna información del proceso remoto que solo está disponible vía rutinas de Windows que no aceptan especificarles un proceso sino que actuan sobre
el proceso dentro del cual se ejecutan (Hay varias que trabajan así)

Programáticamente, el orden de acciones para el tema sería :

1. Definición de los datos necesarios para pasar a nuestra rutina
2. Escritura de la rutina
3. Obtención del manejador del proceso externo al que deseamos inyectar nuestro código
4. Preparación de los datos a enviarle desde nuestro programa
5. Obtención de la memoria, en el proceso externo, que usaremos en nuestro aplicativo
6. Ejecución del proceso
7. Obtención de resultados
8. Liberación de recursos

Pero, conceptualmente, creo mejor empezar discutiendo la ejecución del proceso en sí porque el entender esto abre la puerta para los demás pasos. Veamos:

Ejecutar una rutina en un proceso externo significa básicamente que una rutina compilada dentro del espacio de direcciones de nuestra aplicación vá a ejecutarse en el espacio de direcciones de un proceso que usualmente no hemos escrito nosotros y del cual ni siquiera tenemos los fuentes.

Si consideamos que cuando llamamos rutinas dentro de nuestro programa estamos referenciando a direcciones de memoria en el espacio de direcciones de nuestra aplicación, la primera dificultad que nos salta a la vista es : Y como se localizan esas direcciones desde la rutina que vamos a inyectar si esta estará ejecutandose en un espacio de direcciones diferente ?. Y de ahí se deriva otra : Si la rutina a inyectar tiene su dirección de memoria en nuestro espacio, como puede el otro proceso referenciarla y ejecutarla en su propio espacio ?

Bueno, para responder lo anterior digamos que existe una rutina de Windows llamada CreateRemoteThread que se encarga de ejecutar la nuestra en el proceso externo. Como su nombre lo indica, lo que hace es ejecutar un hilo dentro del proceso remoto, y ese hilo consiste en ejecutar la rutina que inyectamos. Veamos la definición de CreateRemoteThread en Delphi:

Código Delphi [-]
function CreateRemoteThread(hProcess: THandle; 
   lpThreadAttributes: Pointer;
   dwStackSize: DWORD; 
   lpStartAddress: TFNThreadStartRoutine; 
   lpParameter: Pointer;
   dwCreationFlags: DWORD; 
   var lpThreadId: DWORD): THandle; stdcall;

No es mi intención duplicar la explicación que puede encontrarse en Windows, sino comentar los puntos importantes :

hProcess es el manejador del proceso al cual le queremos inyectar la rutina
lpThreadAttributes es un apuntador a una estructura de atributos de seguridad para la ejecución del hilo bajo el cual correrá nuestra rutina. Si se usa Nil, el sistema usará uno por default. A efectos prácticos, a mi me ha funcionado bien con Nil; pero son bienvenidos mayores detalles

dwStackSize es el tamaño de la pila usada por el hilo y para simplicidad, usemos 0 lo que hace que el sistema adopte el mismo tamaño de la pila principal del proceso remoto

lpStartAddress es la dirección inicial de la rutina que queremos inyectar; pero, la dirección inicial que ella tendrá dentro del espacio de direcciones del proceso remoto. En otras palabras, si nuestra rutina se llamara MiRutinaRemota, este parámetro no es @MiRutinaRemota , ya que el @
nos da la dirección; pero dentro de nuestro propio espacio. Queda claro entonces que para pasar este parámetro primero tenemos que averiguar su valor, lo que realmente se hace en el paso 5 que habiamos mencionado. Por otra parte, no puede ser cualquier rutina, tiene que seguir ciertas
reglas que veremos después

lpParameter : Este parámetro representa el único parámetro que podemos pasar desde nuestro programa al proceso externo y no solo es quizás el punto más importante sino también el peor documentado en la ayuda de Windows.

En teoría, puede usarse este valor para pasar un entero a la rutina, o no pasarle nada e indicarlo usando 0; pero, para que la rutina tenga utilidad práctica, el parámetro debe ser un apuntador a una estructura de datos tipo Record que contenga los parámetros de entrada-salida que enviemos a la rutina, y eso no es todo.

La rutina recibirá este parámetro como un apuntador; pero la rutina se ejecuta en otro proceso por tanto, los datos a que apunte este valor deben estar en el espacio de direcciones de ese proceso externo, no en el nuestro. Dicho en otras palabra, si por ejemplo nosotros definimos lo
siguiente:

Código Delphi [-]
Type
    TRemoto = Record
      MIVALOR:     Integer;
      MIRESULTADO: Boolean;
    End;
Var
    MisDatos: TRemoto,

Al momento de llamar a CreateRemoteThread no podemos pasar @MisDatos como lpParameter, por el mismo razonamiento de @MiRutinaRemota. Necesitamos primero que esos datos queden en el espacio de direcciones de nuestro proceso remoto y obtener la dirección que tengan en él; lo que al igual que con la rutina, se hace en el paso 5 mencionado.

Acabo diciendo que dentro de TRemoto lo normal será colocar ciertos valores auxiliares para la lógica de nuestro problema; es decir, no se trata simplemente de colocar ahí solo los parámetros que usualmente tendría nuestra rutina, hay que incluír cierta información adicional que explicaré
cuando describa las reglas que deberá cumplir nuestra rutina

dwCreationFlags brinda facilidades para manipular el hilo. En mi caso, y creo que será lo normal, solo paso 0 porque con eso el hilo se ejecuta enseguida.

lpThreadId es una variable de salida que devuelve el identificador del hilo creado (no muy útil desde mi punto de vista).

La rutina en sí devuelve el manejador del hilo creado; lo que puede usarse desde nuestro programa para determinar cuando se termina el hilo. Igualmente, puede decirnos si hubo errores, ya que en tal caso el valor devuelto es Nil y deberíamos usar GetLastError para obtener más información del error.

Bien entendido, CreateRemoteThread está es ejecutando un hilo y, aún cuando la ejecución empiece enseguida, el control puede devolverse a nuestro programa sin que el hilo, o la rutina remota, hayan terminado; por esto, en nuestro aplicativo deberíamos controlar el fin del hilo usando el
manejador devuelto por la rutina.

Así las cosas, la llamada a CreateRemoteThread sería algo como :

Código Delphi [-]
Var
   AProcess:         THandle;
   AExtFuncAddr:     Pointer;
   AExtDataAddr:     Pointer;
   lpThreadId:       DWORD;
   hThread:          THandle;
Begin
     ...
     ..
     hThread := CreateRemoteThread(AProcess,nil,0,AExtFuncAddr,AExtDataAddr,0,lpThreadId);
     .....
     ...
End;

Bueno, creo que hasta aquí está bien de la primera parte que ya de por sí ha sido muy larga. Voy a esperar a ver si hay quienes se interesan, o puedan aportar, antes de seguír con el resto.
Responder Con Cita
  #2  
Antiguo 08-05-2008
rolandoj rolandoj is offline
Miembro
 
Registrado: abr 2007
Posts: 395
Poder: 17
rolandoj Va por buen camino
Por falta de interés, no seguirá la explicación

Hola,

A todos los que han visitado este hilo les agradezco al menos haberlo hecho y les comentó lo siguiente :

Ha pasado casi una semana y aunque ha habido un número decente de visitas, nadie ha manifestado interes por el tema, en consecuencia no continuaré con la explicación de los restantes puntos.

En lo personal, creo que el tema es técnicamente muy interesante y esta pobremente documentado; pero reconozco que el uso de esta técnica es para situaciones especiales. Como igualmente supongo que la mayoría, al igual que yo, son personas muy ocupadas, entiendo que no le den mayor peso al tema, y en esas circunstancias no se justifica dedicarle tiempo para detallarlo paso a paso
Responder Con Cita
  #3  
Antiguo 11-05-2008
xjre xjre is offline
Miembro
 
Registrado: feb 2008
Posts: 13
Poder: 0
xjre Va por buen camino
Hola, qué tal?

Llevo solo un par de meses programando en delphi y solo un par mas programando en si. Estaba viendo los threads que había en API Windows para ver si en una de esas , alguno servía.
Este me llamó poderosamente la atención por una razón: Lo que se esta haciendo con este procedimiento y el explicado en el link que pusiste, ¿no es "hackear" otra aplicación circundante o estoy confundiendo términos? Me refiero a que según entendí lo que se esta haciendo es meter código nuevo a una aplicación que ya esta corriendo.

Saludos
Responder Con Cita
  #4  
Antiguo 22-11-2008
rolandoj rolandoj is offline
Miembro
 
Registrado: abr 2007
Posts: 395
Poder: 17
rolandoj Va por buen camino
Smile Escritura de la rutina a inyectar

Hola,

En vista de que nadie se interesó, había abandonado este hilo; pero, he recibido una petición personal para continuarlo, así quea quí hay más detalles:

Ya hemos visto la idea central y analizado un poco el punto de arranque, o sea la definición de los datos. pasemos al punto 2, la escritura de nuestra rutina:

También sabemos que la rutina a escribir tiene un solo parámetro que es un apuntador a una estructura tipo Record. Nuestro proceso luce entonces algo como:

Código Delphi [-]
Function ProcesoExterno(dwEntryPoint: PRemoteStruct): longword; stdcall;

Ahora bien, lo crítico aquí es entender que como esta rutina se ejecuta en el espacio de direcciones de otro proceso, cualquier dato no local (o sea, difererente a variables locales de la rutina, al parámetro, u otras rutinas a llamar) tiene que estar en el espacio de direcciones del otro proceso.

Así pués, que pasa si por ejemplo quisieramos llamar a la rutina GetCommandLine de Windows ?

Si intentáramos algo como esto:

Código Delphi [-]
function ProcesoExterno(dwEntryPoint: PRemoteStruct): longword; stdcall;
Var
   TheLine:     String; 
Begin
     ....
     ..
     TheLine := StrPas(GetCommandLine);
     ...
End;

El resultado sería un total desastre. Con lo dicho, es claro que al compilar la dirección de StrPas queda con respecto a nuestro espacio de direcciones, no el del otro proceso, y lo mismo aplica para GetCommandLine.

Y entonces como se hace ?.

La manera de proceder es pasar, como campos de la estructura RemoteStruct, las direcciones, dentro del espacio del otro proceso, de las rutinas que necesitemos llamar. Esto, de todas formas es muy restringido porque solo aplica para ciertas rutinas de Windows, como explicaremos más adelante. Por ahora, concentremonos en como quedaría la rutina;

Código Delphi [-]
function ProcesoExterno(dwEntryPoint: PRemoteStruct): longword; stdcall;
Var
   TheLine:     PChar; 
Begin
     ....
     ..
     TheLine := dwEntryPoint^.DGetCommandLine;
     ...
End;

El cambio de String a PChar es porque, según lo expuesto, no podemos llamar a StrPas, tenemos por tanto que limitarnos al uso de Windows, o sea al PChar.

Observese que se está llamando una Variable, correspondiente a un campo de dwEntryPoint, que debe ser del tipo Function. Por tanto, la definición de RemoteStruct debe tener en cuenta eso. Quedaría algo así:

Código Delphi [-]
Type
    TFncSendMessage = Function(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
    TFncGetCommandLine = Function: PChar; stdcall;
 
    PRemoteStruct = ^TRemoteStruct;
    TRemoteStruct = Record
       ....
       ..
       DSendMessage:           TFncSendMessage;  
       DGetCommandLine:        TFncGetCommandLine;
       ..
       . 
    End;

Para ser más claro, he incluído otra función de Windows, SendMessage, resaltando que se debe definir el prototipo completo de la función, incluyendo los parámetros de la misma

Según esta lógica, es claro que se debe llenar DGetCommandLine en nuestro proceso con la dirección que dicha rutina tenga en el otro proceso. Como obtemo esa dirección ? Lo veremos en la próxima entrega.
Responder Con Cita
  #5  
Antiguo 24-11-2008
alquimista alquimista is offline
Miembro
 
Registrado: ene 2008
Posts: 203
Poder: 17
alquimista Va por buen camino
Talking

creo que la información que estas posteando es muy interesante y valiosa.
Creo que si la gente no contesta es porque no es una cosa muy usual lo que explicas. Y no todo el mundo puede seguir estos temas (incluido yo mismo), aunque tengo interes en este tema.

Por lo que te animo a continuar e incluso pienso que este tema es para hacer un tutorial extenso.

Gracias por ser generoso y compartir tus conocimientos.

Un saludo....
Responder Con Cita
  #6  
Antiguo 30-11-2008
rolandoj rolandoj is offline
Miembro
 
Registrado: abr 2007
Posts: 395
Poder: 17
rolandoj Va por buen camino
Cita:
Empezado por alquimista Ver Mensaje
creo que la información que estas posteando es muy interesante y valiosa.
Creo que si la gente no contesta es porque no es una cosa muy usual lo que explicas. Y no todo el mundo puede seguir estos temas (incluido yo mismo), aunque tengo interes en este tema.

Por lo que te animo a continuar e incluso pienso que este tema es para hacer un tutorial extenso.

Gracias por ser generoso y compartir tus conocimientos.

Un saludo....
Hola,

Gracias por el comentario. Ciertamente, comparto tús apreciaciones.

Quiero disculparme con los que estén interesados en el tema. Sí tengo la intención de terminarlo; pero, un exceso de trabajo me ha impedido elaborar el siguiente punto. Espero hacerlo a mediados de la semana de Diciembre
Responder Con Cita
  #7  
Antiguo 01-12-2008
Avatar de escafandra
[escafandra] escafandra is offline
Miembro Premium
 
Registrado: nov 2007
Posts: 2.195
Poder: 20
escafandra Tiene un aura espectacularescafandra Tiene un aura espectacular
Bueno, cuando alguien se propone publicar tutoriales o explicaciones sobre temas complejos o poco comunes, siempre espera un poco de apoyo. No puedo leer tu hilo sin demostrarte explícitamente mi interés.

Aunque yo programo en C fundamentalmente y apenas domino el delphi, te sigo perfectamente. Veo que te centras, de momento, en la inyección con la API CreateRemoteThread. ¿Has pensado en otras técnicas?. Digo esto porque numerosos antivirus y cortafuegos tienen un Hook a esa API y bloquean la inyección. Y no es que el objetivo sea burlar un antivirus, que de hecho muchos de ellos son burlados inyectando así, sino por curiosidad técnica... Que conste que no pretendo desviar tu exposición.

El tema de inyectar sin dll es más complejo que si lo hacemos con dll recibe mi apoyo por atreverte decidirte en abordar este tema.

Saludos.

Última edición por escafandra fecha: 01-12-2008 a las 01:24:03.
Responder Con Cita
  #8  
Antiguo 01-12-2008
Avatar de Al González
[Al González] Al González is offline
In .pas since 1991
 
Registrado: may 2003
Posts: 5.604
Poder: 29
Al González Es un diamante en brutoAl González Es un diamante en brutoAl González Es un diamante en brutoAl González Es un diamante en bruto
Thumbs up

Hola amigos.

Rolando, acabo de enterarme de la existencia de este interesante hilo. Y pensar que vienes planteando esto desde mayo.

He leído por completo el planteamiento hecho aquí y en el otro hilo que refieres al principio. Francamente desconocía que existiese algo como la función CreateRemoteThread, aunque ahora que lo pienso, quizá ella es una de las claves de cómo funcionan los depuradores.

Ya leí la ayuda de ésta en el archivo de oro Win32.hlp que viene con todo Delphi. Y también encontré esto en Google Code Search. Es un ejemplo muy bien explicado, del cual me llamó la atención esta parte:

Cita:
HMODULE pfnLoadLibrary =
GetProcAddress(LoadLibrary("kernel32.dll"), "LoadLibraryA" );

hThread = ::CreateRemoteThread(hProcessForHooking,
NULL,
0,
pfnLoadLibrary,
"C:\\HookTool.dll",
0,
NULL);

By using GetProcAddress() API we get the address of the LoadLibrary() API.
The dodgy thing here is that Kernel32.DLL is mapped always to the same
address space of each process, thus the address of LoadLibrary()
function has the same value in address space of any running process.
This ensures that we pass a valid pointer (i.e. pfnLoadLibrary)
as parameter of CreateRemoteThread()
.
Sé que buscas inyectar código máquina que no forma parte de una DLL. Pero puse esa referencia porque me llama mucho la atención lo que marqué con negritas.

Por un lado pienso, "ah, pues entonces sólo con DLLs nativas se puede", pero la pregunta que inmediatamente me salta es: ¿Cómo le hace Windows cuando llamamos a una función de su API que recibe una rutina de retrollamada (call back) como parámetro, puesto que solemos usar un simple "@Rutina"?

Y mientras terminaba de escribir esta tonta pregunta ya me respondía a mí mismo: Ah, pues ahí no hay problema porque cuando llamamos a esa función de la API, su DLL fue previamente cargada en el espacio de nuestro propio proceso, así que la dirección dada por el arroba es perfectamente válida.

Pero entonces me queda la inquietud: ¿realmente puede instalarse una rutina de un proceso en ejecución dentro del espacio y contexto de otro proceso? ¿O en la práctica esto de la inyección de código máquina sólo es posible definiendo la rutina en una DLL y haciendo que un LoadLibrary inyectado con CreateRemoteThread cargue esa DLL en el proceso ajeno?

Que interesante tema, en verdad. Estaré pendiente de los avances, esperemos llegar a un feliz desenlace.

Y bueno, me despido por unas horas, porque si no mi chica me enviará a un lugar muy muy remoto.

Un abrazo inyectado.

Al González.

P.D. Una duda más: ¿cómo es posible que el proceso remoto pueda usar el parámetro "C:\\HookTool.dll", si al compilarse el programa esa constante quedaría en el espacio de memoria del proceso inyector?
Responder Con Cita
  #9  
Antiguo 01-12-2008
Avatar de escafandra
[escafandra] escafandra is offline
Miembro Premium
 
Registrado: nov 2007
Posts: 2.195
Poder: 20
escafandra Tiene un aura espectacularescafandra Tiene un aura espectacular
Pues creo que la intención de rolandoj es injectar código sin dll. Si que se puede...
Para poder colocar "algo" en el espacio de direcciones ajeno, tenemos varias API que lo permiten:

OpenProcess // Abre un proceso;
VirtualAllocEx // Reserva memoria en un proceso
WriteProcessMemory // Escribe en la memoria de un proceso reservada por VirtualAllocEx
VirtualFree // Libera la memoria reservada por VirtualAllocEx

Las DLL nativas se cargan siempre en el mismo espacio relativo de cada proceso, no así las demás dlls.

Saludos.
Responder Con Cita
  #10  
Antiguo 01-12-2008
Avatar de Al González
[Al González] Al González is offline
In .pas since 1991
 
Registrado: may 2003
Posts: 5.604
Poder: 29
Al González Es un diamante en brutoAl González Es un diamante en brutoAl González Es un diamante en brutoAl González Es un diamante en bruto
Smile

Hola de nuevo.

Indagando encontré respuestas a las dudas que me surgieron en el mensaje anterior.

1. El código que cité arriba más bien era seudocódigo, el real conlleva complejidades adicionales pero por lo visto superables.

2. Efectivamente, el parámetro lpParameter dado a la función CreateRemoteThread, debe ser una dirección de memoria válida para el proceso remoto. El truco para esto es obtener ésta con la función VirtualAllocEx y escribir en ella el argumento con la función WriteProcessMemory. ¡Por lo visto podemos escribir datos directamente en la memoria de otros procesos!

3. El ejemplo de Rezmond que enlacé arriba es sólo el segundo de al menos dos maneras que él plantea para lograr la inyección. El método 1 no necesita que sea cargada ninguna DLL (aunque por otras razones sí emplea DLLs en el ejemplo que expone).

4. Sí es posible inyectar una rutina de nuestro programa en el espacio de memoria de otro proceso. Como puede apreciarse en el ejemplo del método 1, el truco es, también, utilizar las funciones VirtualAllocEx y WriteProcessMemory para copiar el código máquina de nuestra rutina en la memoria del proceso remoto, y luego usar CreateRemoteThread para crear un hilo dentro de tal proceso, empezando su ejecución en la dirección base del bloque de memoria donde fue copiado el código.

5. En ambos casos, Rezmond ejecuta inicialmente otra función de su autoría, llamada GetDebugPrivs, la cual llama a otro par de funciones de la API de Windows para darle al programa algo así como permisos de depuración. Desconozco en qué punto de sus ejemplos son necesarios tales permisos, pero parece ser algo importante para que aquellos funcionen.

Ahora sólo me quedaría la pregunta, ¿cómo saber cuántos bytes ocupa el código máquina de una función? En uno de los ejemplos vemos que se utilizó un valor de 1000 bytes (cómo diciendo más vale que sobre a que falte), pero ¿podría, por ejemplo, servirnos la ventana CPU del depurador (Ctrl+Alt+C) para calcular el tamaño de una rutina?

Espero con esto haber contribuido en algo para que Rolando encuentre solución al problema que plantea. No quiere decir que me despido del tema, solo que esperaré sus comentarios para ver por dónde podemos seguir ayudando.

Saludos.

Al González.

P.D. Ahora veo que Escafandra hizo un adelanto sobre esto que comenté. Interesante el uso de esas funciones.

Última edición por Al González fecha: 01-12-2008 a las 10:18:28.
Responder Con Cita
  #11  
Antiguo 01-12-2008
Avatar de escafandra
[escafandra] escafandra is offline
Miembro Premium
 
Registrado: nov 2007
Posts: 2.195
Poder: 20
escafandra Tiene un aura espectacularescafandra Tiene un aura espectacular
Cita:
Empezado por Al González Ver Mensaje
Ahora sólo me quedaría la pregunta, ¿cómo saber cuántos bytes ocupa el código máquina de una función?
Pues en C++ es fácil, Si Func1 es la función cuyo tamaño queremos calcular y Func2 es la siguiente en el orden del IDE, sólo tenemos que restar (BYTE*)Func2-(BYTE*) Func1 y el resultado es el tamaño de Func 1 en nº de Bytes una vez compilada.

El problema aparece cuando Func1 llama a su vez a otras funciones que no sean APIs, pues ese tamaño habrá que calcularlo para reservar memoria y "subirlas" a su vez, con lo que en realidad lo que interesa es "subir" no una función sino todo el código que se valla a ejecutar en el nuevo espacio de direcciones. En el caso de hacerlo con dll es el S.O. el que se encarga de todo, pero en nuestro caso debe hacerse manualmente.

Saludos.
Responder Con Cita
  #12  
Antiguo 01-12-2008
rolandoj rolandoj is offline
Miembro
 
Registrado: abr 2007
Posts: 395
Poder: 17
rolandoj Va por buen camino
Smile Gracias por el interés

Hola a todos,

Gracias por el interés. Veo que ahora si se está moviendo el hilo. Dos comentarios rápidos:

1. Una gran disculpa. Como dije antes, infortunadamente, debido a exceso de trabajo, no puedo dedicarme ahora a completar el proceso. Solo a mediados de semana, tendré tiempo libre.

2. Mi problema original si lo resolví. El código que estoy explicando es el que usé y funciona. Las razones para publicarlo son dos: Lo pobre de la documentación disponible, y el hecho de existir un problema en la limpieza final del mismo, el cual, aunque para mi caso no me afecta, si me intriga
Responder Con Cita
  #13  
Antiguo 06-12-2008
rolandoj rolandoj is offline
Miembro
 
Registrado: abr 2007
Posts: 395
Poder: 17
rolandoj Va por buen camino
Smile Preparando los datos a enviar

Hola,

Bueno, al fin tengo algo de tiempo para proseguir con este tema.

Empiezo contestando la pregunta de escafandra:

Si, he usado la técnica de Hook; aunque ya hace bastante tiempo. Lo que ocurre es que cada técnica tiene su ámbito de aplicación. En el caso de los Hook, ellos sirven para monitorear los mensajes de Windows; por ello, pueden enlentecer el sistema, lo que hace que solo se recomiende instalarlos cuando sea necesario. Lo importante es que dependen de que se ejecute el mensaje que se esté monitoreando.

En mi caso, la necesidad era totalmente diferente. Se trataba de que una aplicación debía solicitar a otras determinada información; es decir era un servicio bajo pedido. La lógica seguida era por tanto más adecuada a la inyección de código, donde se llama a la rutina en la aplicación remota obteniendo enseguida la respuesta.

Pasando al tema en si, Veo que ya se ha agregado parte de la información que les traigo; pero creo que de todas maneras les será util.

Habíamos quedado en como obtener las direcciones de las funciones Windows que necesitamos pasar dentro de nuestra estructura TRemoteStruct. Para hacerlo, se procede así:

Definamos una variable global que sea nuestra estructura. Por ejemplo:

Código Delphi [-]
Var
   DataInterProc:       TRemoteStruct;

Esa variable la inicializamos (típicamente en el OnCreate de un formulario), con un código similar a:

Código Delphi [-]
procedure TfrmWPPrincipal.FormCreate(Sender: TObject);
Var
   MUser:               HMODULE;
   MKernel:             HMODULE;
   ::::
   :::
   :: 
begin
     ::::
     :::
     THE_REMOTE_MSG := RegisterWindowMessage(MY_REMOTE_MSG);
     MUser          := GetModuleHandle('user32');
     MKernel        := GetModuleHandle('kernel32');
     DataInterProc.DRegisterWindowMessage := GetProcAddress(MUser,'RegisterWindowMessageA');
     DataInterProc.DGlobalAddAtom         := GetProcAddress(MKernel,'GlobalAddAtomA');
     DataInterProc.DSendMessage           := GetProcAddress(MUser,'SendMessageA');
     DataInterProc.DGetCommandLine        := GetProcAddress(MKernel,'GetCommandLineA');
     StrCopy(DataInterProc.IdRemoteMsg,MY_REMOTE_MSG);
     :::
     ::
end;

Analicemos lo correspondiente a GetModuleHandle y GetProcAddress. La parte clave del asunto es la ya comentada por Al, de su busqueda en Google, que él coloca en negritas.

Debido a que las librerías user32 y kernel32 se cargan en la mismo lugar del espacio de direcciones de cualquier proceso que se ejecute, las direcciones relativas de sus rutinas son las mismas en cualquier proceso. Por lo anterior, es posible obtenerlas en nuestro proceso, para luego pasarlas e invocarlas desde el otro proceso, tal y como se ha explicado en la segunda nota que hice de este tema.

GetModuleHandle, como su nombre lo indica, devuelve el manejador del módulo que le pasamos como parámetro; así pués, lo primero que hacemos es obtener los manejadores de User32 y Kernel32. Luego es solo cuestión de obtener la dirección de cada rutina que nos interese; para lo cual empleamos GetProcAddress. El primer parámetro es obviamente el manejador del módulo; pero el segundo requiere aclaración.

Observarán, por ejemplo, que aparece SendMessageA y no SendMessage. Por qué la A al final ?. Sin detallar mayormente el tema, lo que ocurre es las librerías de Windows a menudo tienen más de una implementación para una función. El nombre que debemos usar es el de la versión adecuada a nosotros. Donde hallamos esa información ?. En nuestro clásico archivo Windows.Pas. Para el caso de la función SendMessage, la línea que aparece es:

Código Delphi [-]
function SendMessage; external user32 name 'SendMessageA';

Lo que nos indica que el nombre requerido para SendMessage es SendMessageA.

Para finalizar esta parte, solo queda mencionar a RegisterWindowMessage.

Esta parte es un mecanismo interesante para que nuestro código inyectado nos devuelva valores, con un mecanismo diferente al de la estructura TRemoteStruct.

La idea es que la información nos sea devuelta por medio de un mensaje de Windows definido por nosotros mismos. Este mecanismo es una alternativa más comoda de manejar cuando hay que devolver varios string de longitud variable e impronosticable, ya que en nuestro esquema, hacer eso por medio del parámetro que pasamos sería complicado.

RegisterWindowMessage devuelve un identificador único a todo Windows para un string que represente un mensaje. La lógica seguída es que si el string que se registra ya existe en Windows, la función devuelve el identificador existente para el mismo. Así pués, lo que hacemos es obtener el identificador en nuestro programa y luego, en la rutina a inyectar, se vuelve a llamar RegisterWindowMessage para obtenerlo de nuevo; pero guardandolo dentro de una variable válida en ese proceso remoto (típicamente, local a nuestra rutina "ProcesoExterno"). Una vez hecho eso ya podemos comunicar ambas aplicaciones vía mensajes propios.

Por lo anterior, si requerimos esa técnica, debemos incluir RegisterWindowMessage dentro de las funciones que deben hacerse disponibles a nuestro "ProcesoExterno".

Quedarían dos puntos por anotar en esta parte. Uno es la definición de MY_REMOTE_MSG y THE_REMOTE_MSG.

El primero es una constante string que debemos asegurar que sea única. Se definiría más o menos así :

Código Delphi [-]
Const
     MY_REMOTE_MSG = 'Mi servicio es AB12456V';

Es decir, un texto que nos indique lo que hace y algunos números y letras que garanticen su unicidad.

El segundo es simplemente una variable del tipo UINT.

El último punto es la llamada :

Código Delphi [-]
StrCopy(DataInterProc.IdRemoteMsg,MY_REMOTE_MSG);

Aquí es la misma lógica ya explicada. MY_REMOTE_MSG está en nuestro espacio de direcciones; como necesitamos usarlo en nuestar rutina remota, debemos incluirlo dentro de los datos a enviarle a ella; por eso, se usa una variable, del tipo arreglo de caracteres, incluida dentro de nuestro registro TRemoteStruct; lógicamente con suficiente espacio para contener a MY_REMOTE_MSG. El Strcopy simplemente se encarga de poner a MY_REMOTE_MSG dentro de nuestro registro.

Bueno, esto ya resultó muy largo; pero espero que les sea util.

En la próxima entrega veremos el proceso de copiado de nuestra rutina.
Responder Con Cita
  #14  
Antiguo 07-12-2008
Avatar de escafandra
[escafandra] escafandra is offline
Miembro Premium
 
Registrado: nov 2007
Posts: 2.195
Poder: 20
escafandra Tiene un aura espectacularescafandra Tiene un aura espectacular
Sigue interesante tu exposición.

rolandoj al leer la respuesta que me diste acerca de los Hooks creo que no me entendiste o que no me expliqué bien. Te comente que algunos antivirus y cortafuegos hacen un Hook a la API CreateRemoteThread, es decir no los Hooks que pone Windows a nuestra disposición para interceptar mensajes o el teclado, sino interceptar la misma API modificando su propio código (mediante inyección)... De esta forma esos programas detectan llamadas a CreateRemoteThread y bloquean las llamadas que consideren ilícitas haciendo inútil la inyección de código por esta vía, la elegida para tu exposición. Existen otras maneras de inyectar código sin usar la API CreateRemoteThread, menos populares y no detectables por los antivirus y cortafuegos y por tanto más universales. Es por este motivo, por lo que te comentaba si no las habías considerado.

Por otra parte, es interesante tu exposición sobre el uso de RegisterWindowMessage. En ocasiones he usado Memoria compartida mediante Mapeos de ficheros en memoria (CreateFileMapping, OpenFileMapping, MapViewOfFile) para compartir bloques de memoria entre diferentes procesos. Con la inyección con dll puedes diseñar funciones en esa dll que pueden ser llamadas desde otro proceso mediante una "microinyección", con lo que te permite comunicar aplicaciones e incluso "bajar" bloques de memoria de otro proceso, una especie de "inyección in¡versa" con lo que la comunicación es bidireccional.

Bueno, no me extiendo mas y espero tu próxima entrega.

Saludos.

Última edición por escafandra fecha: 08-12-2008 a las 22:27:17.
Responder Con Cita
  #15  
Antiguo 22-01-2009
alquimista alquimista is offline
Miembro
 
Registrado: ene 2008
Posts: 203
Poder: 17
alquimista Va por buen camino
Thumbs up Hasta el infinito y mas alla...

Comentar que esto se estaba poniendo interesante.... y se ha parado...

Vulevo a animar a continuar con este hilo.... Todavia me cuesta seguir las explicaciones, pero me he apuntado a esto de la inyeccion y me parece interesantisimo.

Una pregunta esta técnica ¿activa la prevención de ejecución de datos (DEP) de Windows? Probe un ejemplo inyectando en el explorer.exe y se activo.


Un saludo y a ver si poco a poco me entero de algo mas..
Responder Con Cita
  #16  
Antiguo 22-01-2009
Avatar de escafandra
[escafandra] escafandra is offline
Miembro Premium
 
Registrado: nov 2007
Posts: 2.195
Poder: 20
escafandra Tiene un aura espectacularescafandra Tiene un aura espectacular
Cita:
Empezado por alquimista Ver Mensaje
Una pregunta esta técnica ¿activa la prevención de ejecución de datos (DEP) de Windows? Probe un ejemplo inyectando en el explorer.exe y se activo.
Teóricamente Microsoft diseñó el DEP para evitar la ejecución de código desde zonas de memoria no asignadas para ello en las aplicaciones. Los intentos de inyección de código, pueden disparar el DEP.

He realizado pruebas de inyección en el explorer.exe y en en el TaskManager, mediante varias técnicas. Todas las inyecciones las he realizado inyectando dll y con el DEP activado para todas las aplicaciones. En ninguna prueba me ha saltado el DEP. Esto puede ser explicado porque las dll en principio son código, o al menos pueden contenerlo. Posiblemente Microsoft no quiere interferir con ellas porque crearía muchos problemas con aplicaciones lícitas y con su propio sistema de Hooks.

Todas las técnicas de inyección de dll, (salvo las que se aprovechan de la API SetWindowHook para cargar una dll) requieren una pequeña inyección directa de código que es el que se encarga de cargar dicha dll. Ese código se coloca en la memoria del proceso (WriteProcessMemory) como se comenta en este hilo, aunque sólo contenga la API LoadLibraryA. Bien es verdad que es difícil escribir una aplicación que maneje, con la precisión suficiente, la paginación de memoria con permisos de ejecución de código, como para no dejar espacio para esa necesaria microinyección de esa API LoadLibraryA o unas cuantas intrucciones asm puro. Diferente sería el caso de la inyección directa de cantidades mayores de código, que pueden hacer saltar el DEP.

En cualquier caso, tener activo el DEP crea muchas falsas alarmas y molestias al usuario, y suele estar desactivado.

Saludos.

Última edición por escafandra fecha: 22-01-2009 a las 17:30:07.
Responder Con Cita
  #17  
Antiguo 27-01-2009
callbacking callbacking is offline
Registrado
 
Registrado: jul 2008
Posts: 9
Poder: 0
callbacking Va por buen camino
Thumbs up Enormemente interesado, sigue con ello.

De hecho ya habia perdido el interés por no haber conseguido casi nada de lo que quería con la inyección de código, incluso me molesté en aprender a usar Fasm y hacerlo en assembler con ayuda también de Ollydbg, hace casi un año escribí un post intentando conseguir ayuda y he acudido también a otros foros, he abandonado en Septiembre pasado, pero quizá saque ganas de nuevo. Nunca hubo oscuro propósito, intenté obtener los textos de drawtext y textout antes que windows los volcara en pantalla, conseguí hubicar mi función y las direcciones correctas de los saltos, pero no fui capaz de recuperar y modificar los datos sin que petara el sistema. Al igual que tu, me pareció mas sencillo ubicar la función que inyectar una .dll. No conseguí nada,

O SEASE, A MI SI ME INTERESA SEGUIR LEYENDO, EL ALGUNA PARTE ESTA LO QUE NECESITO PARA QUE MI RUTINA FUNCIONE, ES BUENA IDEA ESTE HILO.
CREIA QUE NADIE TENIA INTERÉS POR ALGO TAN INTERESANTE COMO ESTO.
Responder Con Cita
  #18  
Antiguo 27-01-2009
callbacking callbacking is offline
Registrado
 
Registrado: jul 2008
Posts: 9
Poder: 0
callbacking Va por buen camino
Como prueba de mi interés...

Me gustaría disponer de ese código delphi al completo, como prueba no mostraré mis desastres en delphi, mostraré un medio éxito en fasm(si se me permite) que es a propósito del tema en cuestión, debido a las múltiples pruebas que realicé puede contener comentarios de lineas de código ya extintas, asi como variables que no se usan, creo que este era el código que era capaz de inyectar una función en la api drawtextw(tengo varias pruebas), aunque no parece cambiar los textos antes de ser escritos, ... para muestra un botón...
Api Hooking por inyección de código, aproximación en fasm.

Código:
include 'd:\fasm\include\win32ax.inc'




.data
        HookApi      db 'DrawTextW' , 0  ; Nombre del api a hookear
        HookDll      db 'user32.dll'  , 0  ; Nombre de la dll que contiene el api a hookear
        dirapi       dd ?                    ; Dirección del api a hookear
        texto        db 'hello'
 
        process       dd ?                   ; El handle del proceso donde inyectaremos
        pid           dd 1240               ; El pid    del proceso donde inyectaremos--estesalio del taskmanager y fue notepad
     ;PONER PID ADECUADO Y COMPILAR DE NUEVO
        x             dd 5
        ; --------------------api DrawTextW
        ;
        ;-------------------------------------- total 5 bytes si esta es la teoría lo dijo ollydbg y punto.


        ; El número de bytes que tiene al principio el api a hookear (suelen ser 5 ó 7)
 
        buffercall    dd ?                   ; Puntero al buffer para llamar al api (en nuestro propio proceso)
        inibuffer  dd ?                   ; siempre guardada la direccion original del inicio del buffer
        inidirapi dd ?                      ; siempre guardado el inicio del api
        inifun dd ?                         ; siempre guardado el inicio de mi funcion

 
        tamfun        dd ?                   ; Tamaño de la función que suplantará al api


        ;----------------------------------------------------------------------------------------------
        dircode     dd ?                    ; La dirección donde esta escribe la estructura una vez INYECTADA
 
        Prote         dd ?                    ; Necesario para VirtualProtect

        saltoapi     dd ?          ; la direccion calculada a la api+5
        buffsaltoami      dd ?
        tamtotal          dd ?          ;1+tamfun+1+5+5
 
.code
start:
        ; Obtenemos el handle del proceso donde inyectaremos
        mov eax, PROCESS_VM_OPERATION
        or  eax, PROCESS_VM_WRITE
     ;   mov eax,PROCESS_ALL_ACCESS
        invoke OpenProcess, eax, FALSE, [pid]
        mov [process], eax   ;

         ; Obtenemos la dirección del api a Hookear
        invoke GetModuleHandle, HookDll
        invoke GetProcAddress, eax, HookApi
        mov [dirapi], eax       ; vale la dir de la api
        mov [inidirapi],eax     ; siempre guardado sin alterar el inicio

;Vamos a ver , reservamos espacio para pushad+(tamaño de mi funcion)+popad+(5 bytes de la api)+jmp dirapi+5 =
; 1+tamfun+1+5+5
;ahi es na
;a ver el tamaño de la función
        ; Calculamos el tamaño de la función a inyectar
        mov eax, FIN_drawtextexw
        sub eax, Mydrawtextexw
        mov [tamfun], eax



          ;pues eso
          ;  1+tamfun+1+5+5
          ;
        mov eax, 1
        add eax, [tamfun]
        add eax,1
        add eax,[x]
        add eax,5
        mov [tamtotal],eax ; tamtotal tiene el tamaño del buffer
        invoke LocalAlloc, LPTR, [tamtotal] ; espacio local reservado para escritura del buffer
        mov [buffercall], eax  ; aqui la direccion inicio del buffer
        mov [inibuffer],eax ;aqui también para no perderla, por variables va a ser

        mov eax,[buffercall]
        mov byte [eax],0x90 ; 60 pushad     90 es nop
        inc eax
        mov [buffercall],eax ;puesto el opcode  e incrementado el buffer


;ahora a copiar aqui la funcion
        mov ebx,CAMBIO ;direccion de la funcion
        mov [inifun],ebx  ;aqui tambien, no perderla
        invoke RtlMoveMemory, [buffercall],ebx,[tamfun]; la función al buffer
        mov eax,[buffercall]
        add eax,[tamfun]  ; incrementamos el buffer hasta el fin de la funcion
        mov [buffercall],eax
        ; copia la funcion y ademas incrementa buffercall
        ;ahora el opcode de popad 61   90 es nop
        mov eax,[buffercall]
        mov byte [eax],0x90 ;despues de la funcion pues popad o noP
        inc eax
        mov [buffercall],eax ;uno mas para empezar con la api
        ;ahora vienen los 5 bytes de la api

        ;los bytes del api al final del buffer, pero le resto 4, si no no rula
        mov eax, [buffercall]
        sub eax,4
        mov [buffercall],eax
        ; visto con ollydbg


        invoke WaitForInputIdle,[process],1000 ; si señor, esto ya es otra cosa, si no peta.
        ; desde dirapi 5 bytes al buffer
        invoke RtlMoveMemory, [buffercall], [dirapi], [x] ; Copiamos los x primeros bytes del api
        mov eax, [buffercall]
        add eax, [x] ;entonces incrementamos el buffer en x bytes
        mov [buffercall],eax
        ; se supone que ahora lo que falta es el salto a la api+5 (o +4?)
        ;para esto necesito la direccion donde voy a inyectar el codigo

        mov eax, MEM_RESERVE
        or  eax, MEM_COMMIT
        invoke VirtualAllocEx, [process], NULL, [tamtotal], eax, PAGE_EXECUTE_READWRITE
        mov [dircode],eax ;direccion para inyectar el codigo en proceso remoto
        ;dirapi debe estar mas arriba
        mov eax,[dirapi];no ha cambiado, la seguimos usando entonces
        add eax,5 ; ya cambio, dirapi es ahora dirapi+5
        mov ebx,[dircode]
        add ebx,[tamtotal] ; ja! el salto es desde el final del jmp o justo desde antes?
        ; que problema no saberlo, intentemos asi y si no decremento 4 o 5, 4 despues del jmp
        ; antes de los 4 bytes del salto, 5, antes del jmp y los 4 bytes del salto.


        sub eax,ebx
        ; a ver si...    (dirapi+5)-(dircode+tamaño total buffer)=direccion del salto a api+5
        mov [saltoapi],eax
        mov eax,[buffercall]
        mov byte [eax],0xE9
        inc eax
        mov [buffercall],eax  ;insertado opcode jmp  e incrementado buffer
        mov ebx,[saltoapi]
        ; coño, el salto se queda  4 bytes corto!!!!!!!
        ;
        add ebx,4 ;asi supongo que salte donde debe
        mov dword [eax],ebx; con esto se suponen 4 bytes del dword del salto
        ;ahora podemos escribir el buffer en el espacio de la aplicacion
        ;que previamente reservamos
        ;debemos indicar el tamaño total claro
        ;el buffer se supone escrito, ahora lo volcamos en el proceso remoto

        ;invoke VirtualProtect, [dircode-5],[tamtotal+5],PAGE_EXECUTE_READWRITE,NULL


        invoke WriteProcessMemory, [process],[dircode],[inibuffer],[tamtotal],NULL
        ; y ahora claro esta tendremos que insertar el salto en la api
        ; a nuestro codigo
        ; si dirapi esta por encima como coño hago un salto negativo?
        ; y además permisos a los 5 bytes de la api claro esta


        invoke LocalAlloc, LPTR, 5
        mov [buffsaltoami],eax

        mov eax,[dircode]
        mov ebx,[dirapi]
        sub eax,ebx
        sub eax,5; por algun motivo que no se, salta a la dirección
        ;no entiendo yo este problema con los...  si lo pongo salta desde api a pushad
;sino lo pongo salta  bytes mas allá. Asi que ...
        mov [saltoapi],eax  ;mmmmmmm dir del salto


        mov eax,[buffsaltoami]
        mov byte [eax],0xE9
        inc eax
       ; mov [buffsaltoami],eax
        mov ebx,[saltoapi]
        mov dword [eax],ebx



        mov eax, MEM_RESERVE ; eax = MEM_RESERVE | MEM_COMMIT
        or  eax, MEM_COMMIT
        invoke VirtualAllocEx, [process], [dirapi], 5, eax, PAGE_EXECUTE_READWRITE
        invoke WriteProcessMemory, [process], [dirapi], [buffsaltoami], 5, NULL


; y colorin colorado...

        invoke ExitProcess,0








  CAMBIO:
proc Mydrawtextexw dc, lpstring,ncount,lprect,uformat

            nop



            mov byte [lpstring],0x41
            mov byte [ncount],1



            nop  ;referencias mias en la inyección
endp
FIN_drawtextexw:
.end start


- Se supone que la filosofia de esto es:
-en el inicio de la función api se inserta un salto a una función propia.
-después de ejecutar la nueva función, se inserta en esta otro salto a la
api original posterior al salto a nuestra función.
Responder Con Cita
  #19  
Antiguo 27-01-2009
callbacking callbacking is offline
Registrado
 
Registrado: jul 2008
Posts: 9
Poder: 0
callbacking Va por buen camino
Talking Una preguntita ya que estoy

Podrias probar si tu código funciona con drawtextw? Gracias.
Responder Con Cita
  #20  
Antiguo 28-01-2009
Avatar de escafandra
[escafandra] escafandra is offline
Miembro Premium
 
Registrado: nov 2007
Posts: 2.195
Poder: 20
escafandra Tiene un aura espectacularescafandra Tiene un aura espectacular
Hola callbacking. He hecho la prueba que comentabas en tu anterior post, pero inyectando dll. He realizado un Hook a las APIs DrawTextA y DrawTextW. La inyección la he hecho en el notepad, como en tu ejemplo y también en el explorer. No he tenido problemas en capturar el texto o cambiarlo.

El problema mas común al hacer un Hook a una API suele ser la corrupción de la pila, no cuando un programa llama directamente a esa API, sino cuando la llamada se realiza desde otra API. Es fácil darse cuenta si el Hook lo hacemos en ASM y hacemos un debug. En el último ret nos vamos de bareta pues la pila se corrompió.

Los experimentos los he realizado en C y básicamente el sistema ha sido asi:

Código:
// Definición del tipo Puntero a la función API que vamos a Hookear.
typedef INT (__stdcall *PDrawTextW)(HDC hDC, LPCWSTR lpWStr, INT nCount, LPRECT lpRect, UINT uFormat);

// Declaración de un tpunero de ese tipo.
PDrawTextW Ini_DrawTextW = 0;

//............
// Preparación del Hook
// Sustitución de los 5 primeros Bytes de DrawText por JMP HookDrawText
// Ini_DrawText es un puntero a una función con la copia de los Bytes sustituidos de la API original seguido de JMP al 6º Byte de esa API. Para esto se reserva espacio de 10 Bytes mínimo.
//...........

// Función Hookeada:
// Fundamental que sea __stdcall para no corroper la pila....
INT __stdcall HookDrawText(HDC hDC, LPCWSTR lpWStr, INT nCount, LPRECT lpRect, UINT uFormat)
{
   ......
   ......
   // Lamada a la API original. En este punto podemos volver a tomar el control antes de retornar.
   return Ini_DrawTextW(hDC, lpWStr, nCount, lpRect, uFormat); 
}
Existe otro problema, una misma API puede precisar sobreescribir un número diferente de Bytes para producir el salto, dependiendo de las diferentes versiones del S.O. De forma que puede no funcionar en máquinas diferentes. Esto obliga a un estudio particular de la máquina en cuestión o a un desensamblado por código en una función Hook escrita para tal fin.

El tema es engorroso

Saludos.
Responder Con Cita
Respuesta


Herramientas Buscar en Tema
Buscar en Tema:

Búsqueda Avanzada
Desplegado

Normas de Publicación
no Puedes crear nuevos temas
no Puedes responder a temas
no Puedes adjuntar archivos
no Puedes editar tus mensajes

El código vB está habilitado
Las caritas están habilitado
Código [IMG] está habilitado
Código HTML está deshabilitado
Saltar a Foro

Temas Similares
Tema Autor Foro Respuestas Último mensaje
Thread y servidor DCOM externo al proceso ( EXE ) Aldo OOP 1 15-09-2006 18:39:47
Como utilizar un componente externo? Sergei OOP 2 24-01-2006 20:12:24
Inyectar proceso conde API de Windows 4 10-09-2005 16:52:17
Como se puede programar directamente??? Antuan Varios 10 04-08-2005 09:04:38
Como correr un archivo externo? fayala Firebird e Interbase 3 07-04-2005 04:56:00


La franja horaria es GMT +2. Ahora son las 11:52:37.


Powered by vBulletin® Version 3.6.8
Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
Traducción al castellano por el equipo de moderadores del Club Delphi
Copyright 1996-2007 Club Delphi