Hola...

Continuamos con esta cuarta y penultima entrega sobre DLL Injection o Inyeccion de DLL...El secuestro de contexto de subprocesos es una técnica menos utilizada que ofrece una compensación: una forma más sigilosa de realizar la inyección de DLL, pero a costa de una implementación del cargador más compleja. En lugar de crear un nuevo hilo para cargar la DLL, el secuestro del contexto del hilo implica cambiar el estado de un hilo existente para realizar la carga.

Esto se hace suspendiendo primero todos los subprocesos del proceso y luego llamando a [Enlace externo eliminado para invitados] en el subproceso de destino que cargará su DLL. Una vez obtenido el contexto, se cambiarán dos registros: el puntero de instrucción actual, almacenado en el campo Rip de la estructura [Enlace externo eliminado para invitados]  y el puntero de pila actual, almacenados en el campo Rsp.El puntero de instrucción se cambiará para que apunte a una dirección que tiene un fragmento de
instrucciones en ensamblador  que el cargador escribirá en el proceso. Estas instrucciones guardarán el estado del subproceso, llamarán a [Enlace externo eliminado para invitados] para cargar su DLL, restaurarán el estado del subproceso y devolverán la ejecución a la dirección del puntero de instrucción original. , como si nunca hubiera pasado nada.

El puntero de la pila se cambiará para que apunte a un bloque de memoria recién asignado que contendrá el espacio de pila necesario para almacenar el estado del subproceso y proporcionará espacio para LoadLibraryA rutina para llevar a cabo también su funcionalidad interna.

No es obligatorio cambiar el puntero de la pila para realizar el secuestro de contexto; usar la memoria existente donde se encuentra la pila del subproceso también puede funcionar, aunque puede ser más propenso a errores y más difícil de depurar. Las siguientes figuras mostrarán visualmente lo que sucederá al realizar el secuestro del contexto del hilo. Inicialmente, un hilo estará en estado suspendido, con su puntero de instrucción en alguna ubicación de memoria.
Imagen
El estado de un hilo o thread cuando fue suspendido.

El proceso del cargador recuperará el puntero de instrucción y el puntero de pila y utilizará estas dos direcciones para crear las instrucciones auxiliares. Luego, el cargador establecerá el puntero de instrucción en la dirección inicial del código auxiliar. El código auxiliar carga la biblioteca, restaura el antiguo puntero de la pila y devuelve el control al puntero de instrucción original.
Imagen
Controle el flujo después de que se haya escrito el código auxiliar del ensamblado y el subproceso reanude la ejecución.

Obtener el identificador del proceso objetivo

Habiendo visto lo que sucederá, ahora es el momento de implementarlo. El primer paso es controlar el proceso de destino con los [Enlace externo eliminado para invitados] adecuados. El cargador realizará operaciones de suspensión y reanudación en los subprocesos de los procesos de destino y escribirá en el espacio de direcciones de los procesos de destino. Esto requerirá un conjunto de permisos diferente a los necesarios para la implementación [Enlace externo eliminado para invitados].

Código: Seleccionar todo

HANDLE GetTargetProcessHandle(const DWORD processId) {

    const auto processHandle{ OpenProcess(
        PROCESS_SUSPEND_RESUME | PROCESS_VM_OPERATION |
        PROCESS_VM_WRITE | PROCESS_QUERY_INFORMATION, false, processId) };
    if (processHandle == nullptr) {
        PrintErrorAndExit("OpenProcess");
    }

    return processHandle;
}
Recuperar un identificador del proceso de destino.

El permiso PROCESS_SUSPEND_RESUME, como su nombre lo indica, es necesario para realizar con éxito las operaciones de suspensión y reanudación en los subprocesos de los procesos de destino. Al igual que en la implementación de CreateRemoteThreadEx, los otros tres permisos son necesarios para escribir en el espacio de direcciones del proceso de destino.

Suspender el proceso

Con un identificador del proceso, el siguiente paso es suspender todos los subprocesos en ejecución. Hay un par de maneras de hacer esto: el camino largo y el camino corto.

El camino largo implica crear una instantánea de los subprocesos del proceso, enumerar cada subproceso en la instantánea, abrir un identificador para el subproceso y luego suspender el subproceso con una llamada a SuspendThread<. ai=2>. Reanudar cada hilo implicará la misma serie de pasos, pero en su lugar llamará a ResumeThread.

La forma más sencilla es utilizar una API nativa no documentada para suspender o reanudar todo el proceso por usted. Dentro de ntdll.dll hay dos funciones exportadas: NtSuspendProcess y NtResumeProcess. Como sugieren sus nombres, estas funciones suspenderán o reanudarán todos los subprocesos de un proceso. Las definiciones de estas dos funciones se proporcionan a continuación:

Código: Seleccionar todo

using NtSuspendProcessPtr = int(__stdcall*)(HANDLE processHandle);
using NtResumeProcessPtr = int(__stdcall*)(HANDLE processHandle);
Los prototipos de NtSuspendProcess y NtResumeProcess.

Las direcciones de estas API nativas se pueden recuperar desde ntdll.dll a través de GetProcAddress.

Código: Seleccionar todo

template <typename NativeFunction>
NativeFunction GetNativeFunctionPtr(const std::string& functionName) {

const auto ntdllHandle{ GetModuleHandleA("ntdll.dll") };
   if (ntdllHandle == nullptr) {
       PrintErrorAndExit("GetModuleHandleA");
   }

   return reinterpret_cast (
       GetProcAddress(ntdllHandle, functionName.c_str()));
}
Una función genérica para obtener un puntero de función de ntdll.dll.

Recuperando el contexto del hilo

A continuación, se pueden recuperar los contextos del hilo. Como todos los hilos están suspendidos, se pueden cambiar sus contextos. El siguiente código recuperará los contextos de cada hilo en el proceso de destino; estos contextos se devuelven como un par que consta del ID del subproceso y la estructura CONTEXT correspondiente a los subprocesos.

Código: Seleccionar todo

std::vector<std::pair<DWORD, CONTEXT>> GetTargetProcessThreadContexts(
const HANDLE processHandle) {

const std::shared_ptr<HPSS> snapshot(new HPSS{}, [&](HPSS* snapshotPtr) {
PssFreeSnapshot(processHandle, *snapshotPtr);
});

auto result{ PssCaptureSnapshot(processHandle,
PSS_CAPTURE_THREADS | PSS_CAPTURE_THREAD_CONTEXT,
CONTEXT_ALL, snapshot.get()) };
if (result != ERROR_SUCCESS) {
PrintErrorAndExit("PssCaptureSnapshot");
}

const std::shared_ptr<HPSSWALK> walker(new HPSSWALK{},
[&](HPSSWALK* walkerPtr) {
PssWalkMarkerFree(*walkerPtr);
});

result = PssWalkMarkerCreate(nullptr, walker.get());
if (result != ERROR_SUCCESS) {
PrintErrorAndExit("PssWalkMarkerCreate");
}

std::vector<std::pair<DWORD, CONTEXT>> threadIdWithContext{};
PSS_THREAD_ENTRY thread{};

while (PssWalkSnapshot(*snapshot, PSS_WALK_THREADS,
*walker, &thread, sizeof(thread)) == ERROR_SUCCESS) {
threadIdWithContext.push_back(std::make_pair(
thread.ThreadId, *thread.ContextRecord));
}

return threadIdWithContext;
}
La función GetTargetProcessThreadContexts recupera el ID del subproceso y la estructura CONTEXT que lo acompaña.

La función GetTargetProcessThreadContexts funciona creando una instantánea del proceso usando [Enlace externo eliminado para invitados].

Los PSS_CAPTURE_THREADS PSS_CAPTURE_THREAD_CONTEXT [Enlace externo eliminado para invitados] asegúrese de que cada hilo y su estructura CONTEXT se capturen en esta instantánea. Una vez que se crea la instantánea, cada hilo de la instantánea se repite, y un par de ID del hilo y la estructura CONTEXT se guardan en un vector. Una vez que se completa la iteración, el vector contendrá todos los subprocesos y sus contextos y se devuelve a la persona que llama.

Ahora que todos los contextos de subprocesos están disponibles, puede elegir qué subproceso secuestrar para ejecutar el código de inyección. Idealmente, este debería ser un hilo activo, para que la inyección de DLL se realice de inmediato. Si elige un subproceso que está esperando alguna condición, la inyección de DLL puede ocurrir mucho más tarde, si es que ocurre.

El hilo principal del proceso es una buena opción de uso. Como se mencionó anteriormente, el puntero de instrucción (RIP) se cambiará en este hilo. Pero antes de cambiar el puntero de instrucción, es necesario generar el código auxiliar del ensamblado para secuestrar el hilo.

Generando el código auxiliar de secuestro

El código auxiliar del ensamblado necesita cinco datos: la dirección del puntero de pila temporal que se utilizará, la dirección de la función LoadLibraryA , la ruta absoluta de la DLL a inyectar y las direcciones del puntero de instrucción anterior y del puntero de pila para restaurar el estado de ejecución original del subproceso. La siguiente lista muestra la función de generación de resguardos.

Código: Seleccionar todo

auto GenerateHijackStub(
const void* const remoteStackFrameAddress,
const void* const remoteLoadLibraryAddress,
const std::string& fullModulePath,
const DWORD_PTR originalRipAddress,
const DWORD_PTR originalStackPointer) {

std::array<unsigned char, 22> hijackStubPrologue{
/* mov rsp, [remote stack pointer address] */
0x48, 0xBC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,

/* push rax */
0x50,

/* push rcx */
0x51,

/* push rdx */
0x52,

/* push r8*/
0x41, 0x50,

/* push r9 */
0x41, 0x51,

/* push r10 */
0x41, 0x52,

/* push r11 */
0x41, 0x53,

/* pushfq */
0x9C
};

std::array<unsigned char, 27> hijackStubLoadLibrary{
/* lea rcx, [rip + module path offset] */
0x48, 0x8D, 0x0D, 0xCC, 0xCC, 0xCC, 0xCC,

/* mov rdx, LoadLibraryA address*/
0x48, 0xBA, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,

/* sub rsp, 0x40 */
0x48, 0x83, 0xEC, 0x40,

/* call rdx */
0xFF, 0xD2,

/* add rsp, 0x40 */
0x48, 0x83, 0xC4, 0x40
};

std::array<unsigned char, 36 + MAX_PATH + 1> hijackStubEpilogue{

/* popfq */
0x9D,

/* pop r11 */
0x41, 0x5B,

/* pop r10 */
0x41, 0x5A,

/* pop r9 */
0x41, 0x59,

/* pop r8 */
0x41, 0x58,

/* pop rdx */
0x5A,

/* pop rcx */
0x59,

/* pop rax */
0x58,

/* mov rsp, [original stack pointer address] */
0x48, 0xBC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,

/* push low word of original address*/
0x68, 0xCC, 0xCC, 0xCC, 0xCC,

/* mov [rsp+4], high word of original address*/
0xC7, 0x44, 0x24, 0x04, 0xCC, 0xCC, 0xCC, 0xCC,

/* ret */
0xC3,

/* null-terminated space for module path */
0x00
};

const auto stackFrameAddress{ reinterpret_cast<DWORD_PTR>(
remoteStackFrameAddress) + 0x40000 };
std::memcpy(&hijackStubPrologue[2], &stackFrameAddress, sizeof(DWORD_PTR));

const auto loadLibraryAddress{ reinterpret_cast<DWORD_PTR>(
remoteLoadLibraryAddress) };
const auto offsetToModuleName{ 56 };
const auto lowAddress{ static_cast<DWORD>(
originalRipAddress) & 0xFFFFFFFF };
const auto highAddress{ static_cast<DWORD>(
(originalRipAddress >> 32)) & 0xFFFFFFFF };

std::memcpy(&hijackStubLoadLibrary[3], &offsetToModuleName, sizeof(DWORD));
std::memcpy(&hijackStubLoadLibrary[9], &loadLibraryAddress, sizeof(DWORD_PTR));

std::memcpy(&hijackStubEpilogue[14], &originalStackPointer, sizeof(DWORD_PTR));
std::memcpy(&hijackStubEpilogue[23], &lowAddress, sizeof(DWORD));
std::memcpy(&hijackStubEpilogue[31], &highAddress, sizeof(DWORD));
std::memcpy(&hijackStubEpilogue[36], fullModulePath.c_str(),
fullModulePath.length());

return concatenate(hijackStubPrologue, hijackStubLoadLibrary,
hijackStubEpilogue);
}
La función del generador de trozos de ensamblador.

El código auxiliar se divide en tres partes: un prólogo que establece el nuevo puntero de la pila y guarda los registros volátiles* y las banderas, la lógica central que realiza la llamada a LoadLibraryA y un epílogo que restaura los registros volátiles y los indicadores, establece el puntero de la pila en su valor original y luego devuelve el control al puntero de instrucción original.

* Por motivos de brevedad, solo se guardan los registros volátiles de uso general. Una implementación completa también debería salvar los registros volátiles de punto flotante.

Puede notar que el nuevo puntero de pila está escrito con un desplazamiento de su dirección base. Esto se hace para darle a LoadLibraryA un área de búfer en la pila donde él y otras funciones internas que llama pueden construir sus marcos de pila.

El puntero de la pila también se reduce antes de la llamada LoadLibraryA y luego se incrementa de nuevo a su valor original. Esto se hace para evitar que la llamada a LoadLibraryA bloquee los valores de registro de propósito general guardados que se encuentran actualmente en la pila.

El desplazamiento 0x40 que se utiliza es lo suficientemente grande desde el área de la pila donde se almacenan los registros de propósito general como para que LoadLibraryA no los sobrescriba cuando utiliza la pila para llevar a cabo su lógica.

Al final del código auxiliar, el control se devuelve al puntero de instrucción original de una manera poco intuitiva. El contexto del hilo debe ser el mismo cuando se devuelve el control, pero no hay forma de saltar a una dirección absoluta de 64 bits sin utilizar un registro. La alternativa a esto es colocar la dirección en la parte superior de la pila y ejecutar la instrucción ret.

Cuando se ejecuta la instrucción ret, aparecerá el valor en la parte superior de la pila y establecerá el puntero de instrucción ( RIP) a él. Sin embargo, este enfoque también tiene un problema: el ensamblador x64 no tiene forma de insertar un valor inmediato de 64 bits en la pila.

Afortunadamente, esta limitación se puede superar colocando la dirección absoluta en la pila en dos partes: primero la palabra baja de 32 bits de la dirección y luego escribiendo la palabra alta restante en [ Rsp+0x4], que corresponde a los 32 bits superiores.

Ahora, una vez que se ejecuta la instrucción ret, la dirección absoluta se extraerá de la parte superior de la pila y se colocará en el puntero de la instrucción.

Establecer el contexto del hilo de destino
Una vez generado el código auxiliar del ensamblaje, la última parte es establecer el contexto del subproceso de destino para que apunte a la dirección del código auxiliar.

Código: Seleccionar todo

void SetRemoteThreadContext(const DWORD threadId, const void* const newRip,
CONTEXT& context) {

auto threadHandle{ OpenThread(THREAD_SET_CONTEXT,
false, threadId) };
if (threadHandle == nullptr) {
PrintErrorAndExit("OpenThread");
}

context.Rip = reinterpret_cast<DWORD_PTR>(newRip);

auto result{ SetThreadContext(threadHandle, &context) };
if (!result) {
PrintErrorAndExit("SetThreadContext");
}

CloseHandle(threadHandle);
}
Establecer un contexto de subprocesos para reanudar la ejecución en un puntero de instrucción diferente.

Para cambiar el contexto de un hilo, se debe abrir un identificador del hilo con el THREAD_SET_CONTEXT derecho de acceso. Una vez que se obtiene este identificador, cambiar el contexto es solo cuestión de llamar a la función SetThreadContext con el nuevo contexto. Para fines de secuestro de contexto, el campo Rip se cambia para que apunte a la dirección del código auxiliar del ensamblado. Con esta última funcionalidad definida, se puede escribir el cargador.

Código: Seleccionar todo

void InjectWithHijackedThreadContext(const DWORD processId, std::string&
fullModulePath) {

const auto processHandle{ GetTargetProcessHandle(processId) };

const auto NtSuspendProcess{
GetNativeFunctionPtr<NtSuspendProcessPtr>("NtSuspendProcess") };
NtSuspendProcess(processHandle);

const auto threadContexts{
GetTargetProcessThreadContexts(processHandle) };

auto hijackThread{ threadContexts[0] };

const auto* remoteLoadLibraryAddress{ GetRemoteModuleFunctionAddress(
"kernel32.dll", "LoadLibraryA",
processId)};
const auto* remoteFullModulePathAddress{ WriteBytesToTargetProcess<char>(
processHandle, fullModulePath) };

std::array<unsigned char, 1024 * 512> remoteStackFrame{ 0xCC };
const auto* remoteStackFrameAddress{ WriteBytesToTargetProcess<unsigned char>(
processHandle, remoteStackFrame) };

auto hijackStub{ GenerateHijackStub(
remoteStackFrameAddress, remoteLoadLibraryAddress, fullModulePath,
hijackThread.second.Rip, hijackThread.second.Rsp) };

const auto* remoteHijackStub{ WriteBytesToTargetProcess<unsigned char>(
processHandle, hijackStub, true) };

SetRemoteThreadContext(hijackThread.first, remoteHijackStub,
hijackThread.second);

const auto NtResumeProcess{
GetNativeFunctionPtr<NtResumeProcessPtr>("NtResumeProcess") };
NtResumeProcess(processHandle);
}

int main(int argc, char* argv) {

auto fullModulePath{ GetInjectedDllPath("Ch10_GenericDll.dll") };

const auto processId{ GetTargetProcessAndThreadId(
"Untitled - Notepad").first };

InjectWithHijackedThreadContext(processId, fullModulePath);

return 0;
}
La implementación completa del cargador.

El cargador comienza abriendo un identificador para el proceso de destino. Una vez que se obtiene el identificador, todo el proceso se suspende llamando a NtSuspendProcess.

Una vez suspendido el proceso, se recuperan todos los contextos de subprocesos y se elige el contexto del primer subproceso para modificarlo. Luego, la generación del código auxiliar del ensamblado comienza a configurar los parámetros requeridos recuperando la dirección de LoadLibraryA y asignando un bloque de memoria en el espacio de direcciones del proceso de destino que servir como pila temporal.

Estos dos parámetros, junto con la ruta completa del módulo, el puntero de instrucción original y el puntero de pila original, se utilizan para generar el código auxiliar del ensamblaje. Una vez que el cargador genera el código auxiliar, se escribe en el espacio de direcciones del proceso de destino. Por último, el contexto del subproceso de destino se cambia para que apunte a la dirección donde se escribió el código auxiliar del ensamblado y se llama a NtResumeProcess para reanudar la ejecución.

Ejecutando la demostración

Nota: si está utilizando el nuevo Bloc de notas para UWP que se encuentra en la última versión de Windows, deberá cambiar a la versión clásica para que la demostración funcione.

El proyecto ContextHijacking proporciona la implementación completa que se presentó en esta sección. Para probar esto localmente, cree el proyecto GenericDll y el cargador ContextHijacking. proyecto. Después de una compilación exitosa, inicie el Bloc de notas y luego la aplicación de carga.

Verá el conocido mensaje "¡DLL inyectada!" cuadro de mensaje emergente. No descarte este cuadro de mensaje todavía. En su lugar, abra Process Hacker, busque el proceso notepad.exe y y navegue hasta su pestaña Hilos. A diferencia del ejemplo de la sección anterior, donde se creó un nuevo hilo mediante una llamada CreateRemoteThreadEx, no se creó ningún hilo nuevo.
Imagen
La pestaña Hilos o Threads de notepad.exe

Sin embargo, al buscar en la pestaña Módulos se revelará que GenericDll.dll se cargó, como se muestra a continuación.
Imagen
La DLL GenericDll.dll se ha inyectado en notepad.exe.Esto muestra que la DLL GenericDll.dll se inyectó en notepad.exe. y el cuadro de mensaje se está ejecutando en el contexto del proceso del Bloc de notas.Si tiene curiosidad por ver la ejecución del código auxiliar de secuestro de contexto, puede rastrearlo con un depurador, aunque los pasos son un poco más complicados. Reinicie la aplicación Bloc de notas y conéctela con [Enlace externo eliminado para invitados] abriendo x64dbg y navegando a Archivo -> Adjuntar desde la barra de menú. En el cuadro de diálogo Adjuntar, ingrese notepad.exe y seleccione el proceso Bloc de notas.
Imagen
El proceso del Bloc de notas está disponible para adjuntarlo en x64dbg.

Asegúrese de que x64dbg no esté roto y de que el proceso del Bloc de notas se esté ejecutando. En Visual Studio, establezca un punto de interrupción en la llamada NtResumeProcess e inicie el cargador. Cuando se alcance su punto de interrupción en Visual Studio, el proceso del Bloc de notas seguirá en estado suspendido y tendrá la oportunidad de rastrear el código auxiliar de secuestro de contexto. Pase el cursor sobre la variable targetHijackStub y copie su dirección, como se muestra a continuación.
Imagen
Copiando la dirección de targetHijackStub.

Vuelva a x64dbg. Haga clic en la ventana principal que muestra las instrucciones desensambladas y presione Ctrl + G para abrir el cuadro de diálogo "Follow Expression".
Imagen
Elegir una expresión a seguir o ver.

Ingrese la dirección de targetHijackStub en este cuadro de diálogo y presione Entrar. Esto le llevará al lugar de la memoria donde se asignó y escribió el código auxiliar de secuestro de contexto. Deberías ver las instrucciones para el código auxiliar de secuestro. Haga clic en la primera instrucción y presione F2 para establecer un punto de interrupción. La dirección se resaltará en rojo una vez que se establezca el punto de interrupción.
Imagen
El código auxiliar de secuestro de contexto en el proceso notepad.exe.

Vuelva a Visual Studio y continúe con la ejecución del cargador. Esto completará la llamada NtResumeProcess y la aplicación Bloc de notas seguirá ejecutándose nuevamente. Una vez que el cargador haya finalizado la ejecución, regrese a x64dbg. Se debe alcanzar el punto de interrupción que estableció al inicio del código auxiliar de secuestro de contexto. Esto se indicará mediante el cambio del color de la instrucción, como se muestra a continuación:
Imagen
El punto de interrupción al inicio del trozo que se está alcanzando.

En este punto, puede continuar con las instrucciones en x64dbg. Cuando se ejecuta la instrucción call rdx en el código auxiliar, se cargará la DLL GenericDll.dll y verá el mensaje “¡DLL inyectada!” cuadro de mensaje emergente.

Bueno,pues hasta aqui esta cuarta entrega, puede que haya algun error con los codigos,pues la funcion de insertar codigo del foro no funciona bien y hay que hacerlo a mano...igual se me ha escapado algun codigo o hay algun error...si alguien lo prueba que comente al respecto para solucionarlo.

Nos vemos en la quinta y ultima entrega...

Saludos a tod@s...

cLn


 
 
Imagen


Solo lo mejor es suficiente...
Responder

Volver a “Manuales”