Charla de Code Inject y DLL Inject - By ^n0b0dy^

El Inject es como el nombre sugiere, inyectar, insertar, meter codigo dentro de otra aplicacion (otro proceso mejor dicho) para tomar cierto control sobre esta. Al inyectar codigo, podremos leer todo lo que la aplicacion target tiene en memoria, ya que estariamos corriendo sobre su misma direccion de memoria, y tendriamos su mismo acceso de seguridad.

Asimismo, soy un newbie en inject todavia, pero puedo enseñarles lo que se, que realmente veo muy util.

Conozco 3 tipos de Inject, pero voy a explicar en profundida solo los 2 que aprendi y aplique: El Dll inject y el Code inject

Para esto, se utilizan 2 apis que corresponden a sistemas NT5+: VirtualAllocEx() y ]CreateRemoteThread()

Aqui van los prototipos:

LPVOID VirtualAllocEx(

HANDLE hProcess, // process within which to allocate memory

LPVOID lpAddress, // desired starting address of allocation

DWORD dwSize, // size, in bytes, of region to allocate

DWORD flAllocationType, // type of allocation

DWORD flProtect // type of access protection

);

 

HANDLE CreateRemoteThread(

HANDLE hProcess, // handle to process to create thread in

LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security attributes

DWORD dwStackSize, // initial thread stack size, in bytes

LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function

LPVOID lpParameter, // argument for new thread

DWORD dwCreationFlags, // creation flags

LPDWORD lpThreadId // pointer to returned thread identifier

);

Suena dificil? No se asusten, son muy faciles de usar para nuestro proposito.

VirtualAllocEx() asigna memoria en el espacio de memoria virtual de la aplicacion; Esto significa el sector donde esta el codigo ejecutable de la aplicacion, las variables, etc. La version 'Ex' es para asignar memoria en un proceso remoto.

CreateRemoteThread() creo que queda bastante claro, crea un thread en un proceso remoto, siempre y cuando la direccion de la funcion sea LOCAL al proceso remoto, osea, exista alli o que el proceso remoto tenga acceso a esa direccion de memoria (vale aclarar que debe ser una region de memoria ejecutable).

 

Veamos los 2 tipos de inject de la clase:

Dll Inject

Dll Inject se le llama a la tecnica de lograr que otro proceso cargue una libreria que nosotros especifiquemos, aun despues de haberse ya iniciado y estar corriendo normalmente.

La teoria es esta: Asignamos memoria en el proceso remoto, copiamos alli el string con el path de la dll que queremos cargar y luego creamos el thread remoto...llamando a [b]LoadLibrary[/b]()!

"LoadLibrary??" si man, leiste bien.

Si se fijan, THREAD_START_ROUTINE (o ThreadProc) tiene el siguiente prototipo:

 

DWORD WINAPI ThreadProc(

LPVOID lpParameter // thread data

);

...mientras que LoadLibrary() usa este:

 

HINSTANCE LoadLibrary(

LPCTSTR lpLibFileName // address of filename of executable module

);

...hum! Si los ves por arriba, tienen poco parecido, pero si empiezas a ver los valores... retorno de 32 bits, usan 1 solo parametro, tb, de 32 bits...muy parecidos! Es mas! con un poco de type casting, serian iguales! :) y ahi es donde nosotros aprovechamos!

 

Vamos a codear este procedimiento:

BOOL RemoteLoadLibrary(DWORD dwPid,char *pLibPath) {

HANDLE hProc, hThread;

void *pPath;

 

// Abrimos el proceso, con los accesos adecuados

hProc = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE,false,dwPid);

 

if (hProc != INVALID_HANDLE_VALUE) {

// Asignamos memoria en el proceso remoto. Esta memoria existe en el contexto del

// proceso remoto, asi que este puede leerla sin problemas

pPath = VirtualAllocEx(hProc,0,strlen(pLibPath) + 1,MEM_COMMIT,PAGE_READWRITE))

 

if (pPath != NULL) {

// Copiamos el string

if (WriteProcessMemory(hProc,pPath,pLibPath,strlen(pLibPath) + 1,NULL)) {

 

// Ejecutamos loadlibrary en el proceso remoto, usando como parametro el string con el path de la libreria

if ((hThread = CreateRemoteThread(hProc,NULL,NULL,(DWORD (__stdcall*)(void *))LoadLibrary,(void*)pPath,NULL,NULL)) != INVALID_HANDLE_VALUE) {

 

// No necesitamos mucha mas comprobacion, solo saber cuando termina

WaitForSingleObject(hThread);

// Cerramos el handle

CloseHandle(hProc);

return true;

}

}

 

// Liberamos la memoria que asignamos, ya que el proceso

// remoto no la va a tener en cuenta

VirtualFreeEx(hProc,pPath,0,MEM_RELEASE);

}

// Cerramos el handle

CloseHandle(hProc);

}

 

return false;

}

 

(Nota: no lo testie, puede que salte algun typecast mal en algun lado, el codigo en si es el correcto :))

 

Como veran, es MUY facil hacer dll inject, y tiene algunas utilidades copadas, como poder correr codigo en un proceso ajeno (imaginensen correr codigo que use winsock, sobre el iexplore? podrian bypasear el fw ;), hacer algunos hooks o cambiar interfaces de los programas, ya que tendrias acceso a todos sus hwnd komo si fueran locales (pk lo son).

Aun asi, este metodo es facil y util, pero es muy visible para un worm, ya que necesitaran un exe, que actuaria de loader (este codigo anterior) y un dll que haria el trabajo pesado sobre el proceso target. Como mejorar esto? Code Inject!

Code Inject

Este metodo, es bastante mas complicado y limitado de implementar, ya que un problema lo limita...voy a tratar de explicarlo lo mas claro posible, pero sin un minimo conocimiento sobre PE, por ahi no lo entienden; Lo voy a explicar un toque mas adelante, no se preocupen, no influye tanto.

La diferencia con Dll Inject es significativa: no usamos una dll externa...copiamos las funciones que vamos a usar, desde la memoria de nuestro proceso a la memoria del proceso remoto (o target). En si, es factible, pero tienen que cumplir un par de condiciones y no es muy sano de hacer...

Explico la teoria: al igual que Dll Inject, asignamos memoria en el proceso remoto, pero esta vez, con 2 proposito: uno es para tener lugar donde alojar nuestra funcion, ya que debe estar COMPLETA en el proceso remoto, y la otra, para asignar memoria para la struct (ahora veran por que) que llevara los datos a la funcion en el proceso remoto (lo que en el ejemplo anterior era la direccion de la libreria). El resto es muy parecido, se copia funcion y parametros, y se ejecuta la funcion en el proceso remoto con CreateRemoteThread() (pueden o no necesitar esperar para liberar la memoria asignada, dependiendo de sus propositos).

 

Ok, es todo muy lindo, pero la verdad, este sistema, es mucho mas limitado que el Dll Inject y ahora les explico por que. Vamos a informatizarnos un poco:

 

(Nota: si saben sobre PE, no hace falta que lean)

Cada executable que el SO carga, no es solo meter el programa en memoria y ejecutarlo. El SO, hace muchas cosas: Alinea las seccion con la memoria, consigue y reemplaza las direcciones de memoria de las funciones de dll estatica, crea y asigna el acceso para cada seccion con sus atributos necesarios, etc.

Nuestro problema, reside en la IAT o Import Address Table. Esta contiene las direcciones de memoria (o a veces redirecciones de memoria) de las funciones que nuestro programa importa implicitamente (cuando usamos dll estaticas, osea, siempre), y las reemplaza en nuestro codigo, para que cuando llamemos a [b]Sleep[/b]() vaya hacia esa funcion. El loader del SO carga esa tabla y la modifica y setea las direcciones correctas.

Esto nos afecta, y no hay nada mas explicativo que un ejemplo de por que:

 

DWORD DormiteUnRato(DWORD dwSecs) {

Sleep(dwSecs * 1000);

 

return dwSecs;

}

Para que sea mas facil de entender, voy a exagerarlo bastante (no es tan tan asi):

 

 

DWORD 0x37473892(0x45060540) {

0x75182941(0x45060540 * 1000);

 

return 0x45060540;

}

Cada valor que no sea estatico y constante tiene un direccion en memoria, que es asignada por el compilador si es parte de nuestro codigo, o, por el loader si es parte de una funcion de una dll estatica. Si nosotros copiamos una funcion que tiene NUESTRAS direcciones de memoria, cuando el proceso remoto la ejecute, por mas que el codigo de la funcion este en su propia direccion de memoria, las llamadas no lo estaran y va a tratar de llamar a direcciones dentro de la memoria de NUESTRO programa inyector! Cosa 'e mandinga! (osea, alto access error 00005). Obviamente, no se puede. Como esquivamos este bache? pasandole las direcciones de funcion correctas y siendo precavidos, sabiendo que las librerias normales (user32.dll, kernel32.dll, etc) tienen la misma direccion de memoria para todos los procesos (osea, la funcion Sleep() tiene el mismo address en nuestro proceso y en el remoto, siempre y cuando la llamemos explicitamente).

Otro problema (o casi problema) que nos afecta, es que si vamos a copiar una funcion entera, como sabemos cuanto copiar? que tamaño en memoria ocupa esa funcion? Cuanto memoria asignamos en el proceso remoto? :? En general, para sobrepasar estos dilemas, se elije un valor constante, que, masomenos, nos de la seguridad que vamos a copiar al menos toda nuestra funcion (aunque copiemos de mas). :idea: Despues les voy a dar una solucion copada que encontre :) (aunque no es tan necesario implementarla).

Ok, aca estaria un codigo simple de Code Inject:

#define FUNCMAX 1024 // Tamaño uficiente para nuestra funcion simple

 

typedef struct {

void (WINAPI *fnSleep)(DWORD);

int (WINAPI *fnMessageBox)(HWND,char*,char*,UINT);

DWORD dwSecs;

char cMsg[256];

char cTitle[256];

} tINFO;

 

DWORD FuncionQueCopiamos(tINFO *pInfo) {

pInfo->fnSleep(pInfo->dwSecs * 1000);

 

int iRet = pInfo->fnMessageBox(NULL,pInfo->cMsg,pInfo->cTitle,MB_YESNO);

 

return iRet;

}

 

FuncionInyectora(DWORD dwPid) {

HANDLE hProc, hThread;

void *pFunc, *pData;

tINFO stInfo;

DWORD dwRet;

 

// Abrimos el proceso, con los accesos adecuados

hProc = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE,false,dwPid);

 

if (hProc != INVALID_HANDLE_VALUE) {

// Asignamos memoria en el proceso remoto. Fijensen, que difieren en el tipo de

// acceso que pedimos, ya que la funcion necesita EXECUTE access, pero los datos

// solo con read y write nos alcanza.

 

// Memoria asignada para la funcion

pFunc = VirtualAllocEx(hProc,0,FUNCMAX,MEM_COMMIT,PAGE_EXECUTE_READWRITE))

 

pData = VirtualAllocEx(hProc,0,sizeof(tINFO),MEM_COMMIT,PAGE_READWRITE))

 

if (pData && pFunc) {

 

// Seteamos los valores que vamos a pasarle (no miren el typecast, es solo

// para resumir el codigo)

strcpy(stInfo.cMsg,"Entendiste???");

strcpy(stInfo.cMsg,"Remote Code");

*((void**)&stInfo.fnSleep) = GetProcAddress(GetModuleHandle("kernel32.dll"),"Sleep");

*((void**)&stInfo.fnMessageBox) = GetProcAddress(GetModuleHandle("user32.dll"),"MessageBoxA");

stInfo.dwSecs = 5;

 

// Copiamos la funcion (otro typecast bonito)

WriteProcessMemory(hProc,pFunc,*((void**)&FuncionQueCopiamos),FUNCMAX,NULL);

 

WriteProcessMemory(hProc,pData,&stInfo,sizeof(tINFO),NULL);

 

// Aqui es donde cambia: en vez de llamar a LoadLibrary(), llamaremos a pFunc(), ya que esta contiene nuestro codigo

if ((hThread = CreateRemoteThread(hProc,NULL,NULL,(DWORD (__stdcall*)(void *))pFunc,pData,NULL,NULL)) != INVALID_HANDLE_VALUE) {

 

// No necesitamos mucha mas comprobacion, solo saber cuando termina

WaitForSingleObject(hThread);

 

// Obtenemos el retorno

if (GetExitCodeThread(hThread,&dwRet)) {

switch (dwRet) {

case MB_YES : {

MessageBox(NULL,"El usuario respondio: Yes","Code Inject",MB_OK);

break;

}

case MB_NO : {

MessageBox(NULL,"El usuario respondio: No","Code Inject",MB_OK);

break;

}

}

}

 

// Cerramos el handle

CloseHandle(hProc);

return true;

}

// Liberamos la memoria

VirtualFreeEx(hProc,pFunc,0,MEM_RELEASE);

 

VirtualFreeEx(hProc,pData,0,MEM_RELEASE);

}

// Cerramos el handle

CloseHandle(hProc);

}

 

return false;

}

En general, uds como ven no pueden usar dentro de la funcion ninguna llamada a una funcion o region de area externa, ya que no seria valida dentro del contexto del proceso remoto. Aun asi, se pueden definir variables estaticas, como el 'int' que mostre colgado ahi, ya que es una referencia dentro del contexto de la funcion, y se copia junto con ella. Tb pueden pasar solamente las address de LoadLibrary y GetProcAddress y asi poder usar mas apis en la funcion remota, cargandolas dinamicamente.

 

Hay un tercer metodo que conozco pero que no aplique, y se llama Thread Hijacking, y lo voy a comentar solo un poco: se trata de ejecutar un proceso en modo SUSPENDED, usando CreateProcess, y asi, conseguir el Thread Id principal (el id del thread primario). Usando GetThreadContext() podemos conseguir los valores de registros eip, eax, etc. Con eso, hacemos lo mismo que el Code Inject: copiamos la funcion y sus datos, pero, esta vez, en vez de llamar a CreateThread(), cambiamos el registro EIP (que contiene la posicion donde se encuentra ejecutando el thread) y lo apuntamos hacia la address de inicio de nuestra funcion copiada. Se hacen algunos arreglos mas (que no preste mucha atencion, pero uno es insertar un salto desde nuestra funcion hacia la verdadera direccion EIP) y luego se resume el proceso, y entonces nuestra funcion se ejecuta, y le deja el control a la verdadera funcion thread. Las ventajas que tiene este sistema es que algunos firewall controlan la api CreateRemoteThread(), con lo cual, el metodo anterior, no funcionaria. Ademas, no depende de CreateRemoteThread(), api que solo es soportada por sistemas NT5+ (2k,xp,2003,vista?) y asi el Inject se puede lograr aun en Windozes9x. Si llego a aprender este metodo, se los comentare mas adelante.

 

:Ultimo punto: sobre la manera de encontrar el tamaño verdadero de una funcion, les doy un metodo que descubri, muy simple y util:

// Formato para poder leerla con memcmp

BYTE ENDFUNCTIONSTR[] = { 0x54, 0x65, 0x73, 0x74, 0x69, 0x6E, 0x67, 0x45, 0x4F, 0x46, 0x00 };

// Nuestra 'bengala'; Nos va a avisar cuando llegamos al final =)

#define ENDOFFUNCTION __asm _emit 0x54 __asm _emit 0x65 __asm _emit 0x73 __asm _emit 0x74 __asm _emit 0x69 __asm _emit 0x6E __asm _emit 0x67 __asm _emit 0x45 __asm _emit 0x4F __asm _emit 0x46 __asm _emit 0x00

 

// Consigamos el tamaño

DWORD FindFuncSize(void *pStart,DWORD dwMax = 4096) {

 

// Seteamos un puntero, para movernos mas comodamente

BYTE *pPnt = (BYTE*)pStart;

 

// Loopeamos

for (DWORD dwCount = 0; dwCount < dwMax; dwCount++) {

 

// Es pPnt == a nuestra 'bengala'?

if (memcmp(pPnt,(void*)ENDFUNCTIONSTR,sizeof(ENDFUNCTIONSTR)) == 0) {

// Si! aqui esta!

break;

}

 

// Continuemos

pPnt++;

}

 

// Restemosle a la posicion encontrada la posicion inicial y tendremos

// el tamaño de la funcion (si pPnt == NULL significa que llegamos a dwMax,

// y retornaremos ese valor

return (pPnt != NULL ? (DWORD)(pPnt - (BYTE*)pStart) : dwMax);

}

 

BOOL Test(void) {

printf("Hello World\n");

 

return true;

 

// Aqui esta el truco

ENDOFFUNCTION

}

 

void CheckSize(void) {

DWORD dwSize;

 

dwSize = FindFuncSize(Test);

 

printf("La funcion Test() mide %u bytes de largo\n",dwSize);

}

 

'_asm _emit' pone en memoria bytes, para que sirvan de 'bengala', sin importarle si son o no funciones de maquina (asm) validas. Como nosotros los agregamos despues del ultimo return, no afecta la funcionalidad del codigo, y haciendo un loop, podemos encontrar nuestra 'bengala': en ese lugar termina nuestra funcion; Si a esa posicion le restamos el inicio de la funcion, tendremos el tamaño total de la funcion :) (Nota: dwMax es solo por precaucion, no keremos seguir indefinidamente) (Nota2: la 'bengala' deberian ser bytes unicos, no deberian repetirse en el resto del codigo)

 

^n0b0dy^