Bien, dije que buscaría una respuesta y aquí está, como prometí.
En primer lugar, quería crear un objetivo real para jugar. No hay nada como un ejemplo funcional, que se pueda tocar, y esto es en gran medida un proceso manual. Entonces, sin más dilación, compilé esto en MSVC con /MT
:
#define WINVER 0x501
#define _WIN32_WINNT 0x0501
#define _WIN32_WINDOWS 0x0501
#define _WIN32_IE 0x0501
#define UNICODE
#include <wchar.h>
#include <windows.h>
int APIENTRY wWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
MessageBox(NULL, L"Hello, world.", L"A messagebox", MB_OK | MB_ICONEXCLAMATION);
return 0;
}
Esperamos que esta bestia importe user32.dll!MessageBoxW
y, efectivamente, lo hace.
Para encontrar esto dentro del ejecutable mismo, usemos WinDbg. Uso WinDbg por dos razones: en primer lugar, no puedo pagar IDA y, en segundo lugar, es el mismo depurador que usaría para un controlador de kernel (incluso con IDA, que usa su motor), por lo que es bueno, aunque un poco no intuitivo.
Compilé mi ejecutable a UnpackSimple.exe
para la arquitectura x86, ya que UPX no es compatible con Win64. Ahora, lo primero que notará cuando ejecute esto bajo WinDbg es esta línea:
ModLoad: 01360000 01370000 UnpackSimple.exe
Allí mismo, vemos el rango de direcciones al que se ha asignado UnpackSimple.exe
. Esto es importante, porque todas las direcciones en su encabezado son relativas a 01360000
.
Así que ahora, para encontrar la información del encabezado, simplemente podemos solicitarla:
!dh UnpackSimple.exe
OPTIONAL HEADER VALUES
10B magic #
11.00 linker version
5E00 size of code
6C00 size of initialized data
0 size of uninitialized data
1193 address of entry point
1000 base of code
----- new -----
01360000 image base <--- this is the image base address.
1000 section alignment
200 file alignment
2 subsystem (Windows GUI)
6.00 operating system version
0.00 image version
... blah blah blah ....
0 [ 0] address [size] of Export Directory
8D54 [ 3C] address [size] of Import Directory
D000 [ 1E0] address [size] of Resource Directory
0 [ 0] address [size] of Exception Directory
0 [ 0] address [size] of Security Directory
E000 [ 6D0] address [size] of Base Relocation Directory
7150 [ 38] address [size] of Debug Directory
0 [ 0] address [size] of Description Directory
0 [ 0] address [size] of Special Directory
0 [ 0] address [size] of Thread Storage Directory
89D8 [ 40] address [size] of Load Configuration Directory
0 [ 0] address [size] of Bound Import Directory
7000 [ 108] address [size] of Import Address Table Directory
0 [ 0] address [size] of Delay Import Directory
0 [ 0] address [size] of COR20 Header Directory
0 [ 0] address [size] of Reserved Directory
Oh mira, un IAT! Vive en 01360000+7000
y podemos persuadir a WinDbg para que nos brinde esa información de una manera bastante sencilla. El comando dps
que estamos a punto de usar es corto para dump pointers, dereferencing with symbols
, toma una dirección de argumento y un espacio por dirección de entrada.
dps 01360000+7000 L108/4
01367000 75b3d7ea kernel32!TerminateProcessStub
01367004 75b23f3c kernel32!CreateFileWImplementation
01367008 75b2520b kernel32!GetCommandLineWStub
... <snip> ...
013670f4 75b47ab2 kernel32!WriteConsoleW
013670f8 75b21410 kernel32!CloseHandleImplementation
013670fc 00000000
01367100 76bafd3f USER32!MessageBoxW
01367104 00000000
Puede ver algunas características importantes del IAT aquí. Primero está la dirección en el lado izquierdo - dirección de entrada IAT. La dirección a la izquierda es la dirección en memoria de la DLL. Por último, tenga en cuenta que hay algunas entradas cero. Cada importación de DLL termina con una entrada cero, lo que indica al cargador que no hay más entradas para cargar.
Ahora, veremos el desmontaje de la función principal. Para hacer esto, escriba estos dos comandos
bp UnpackSimple!wWinMain
g
Luego, irá al ingreso al wWinMain
anterior. Ahora, si te fijas bien:
UnpackSimple!wWinMain:
01361000 6a30 push 30h
01361002 68a0893601 push offset UnpackSimple!'string' (013689a0)
01361007 68bc893601 push offset UnpackSimple!'string' (013689bc)
0136100c 6a00 push 0
0136100e ff1500713601 call dword ptr [UnpackSimple!_imp__MessageBoxW (01367100)]
01361014 33c0 xor eax,eax
01361016 c21000 ret 10h
Como era de esperar, existe nuestra función, _imp__MessageBoxW
, que hace referencia a una buena entrada IAT :)
Así que ahora la pregunta es ¿cómo diablos vamos hacia atrás? Bueno, para construir primero algo que simplemente no funciona, necesitamos conseguir un ejecutable empaquetado.
Para ese fin, ejecuté upx.exe -o UnpackSimplePacked.exe UnpackSimple.exe
para producir un binario empaquetado agradable. Cargue este binario en WinDbg y lo primero que notará es que:
!dh UnpackSimplePacked.exe
No hace absolutamente nada. Sin embargo, podemos tener en cuenta que:
ModLoad: 01220000 01233000 image01220000
Se ha visto
, por lo que la imagen que necesitamos está ahí, en la memoria. Simplemente no existe como una imagen de disco. En esta etapa, supongo que upx
en realidad carga los DLL directamente, en lugar de usar el NT Loader. Podemos probar esto de dos maneras: en primer lugar, si escribe sxe ld
en WinDbg, se interrumpirá en todas las cargas de módulos. No se producen roturas para user32.dll
, ni está cargado.
En segundo lugar, como querías ver:
0 [ 0] address [size] of Import Address Table Directory
Está completamente vacío.
Sin embargo, si revisamos la lista de módulos cargados:
lm
start end module name
00210000 00223000 image00210000 (deferred)
74c20000 74c2c000 CRYPTBASE (deferred)
74c30000 74c90000 SspiCli (deferred)
74d80000 74e20000 ADVAPI32 (deferred)
74eb0000 74f4d000 USP10 (deferred)
74f50000 74f5a000 LPK (deferred)
75030000 750dc000 msvcrt (deferred)
755a0000 755b9000 sechost (deferred)
75720000 757b0000 GDI32 (deferred)
757e0000 758d0000 RPCRT4 (deferred)
75b10000 75c20000 kernel32 (deferred)
76ab0000 76af7000 KERNELBASE (deferred)
76b40000 76c40000 USER32 (deferred)
77550000 776d0000 ntdll (pdb symbols)
Como puede ver, USER32
tiene espacio asignado listo para funcionar.
Ahora aquí hay algunos trucos. Abrí otra ventana de WinDbg con mi versión UnpackSimple.exe y encontré la dirección de user32!MessageBoxW
. Una vez que obtuve esto, encontré que era 76bafd3f
desde un desplazamiento base de 76b40000
, lo que me da un desplazamiento de 6FD3F
. En mi versión empaquetada, establecí un punto de interrupción en bp 76b40000+6FD3F
y, efectivamente, ¡había un seguimiento de la pila!
k
003cfd0c 013a1014 USER32!MessageBoxW
WARNING: Stack unwind information not available. Following frames may be wrong.
003cfd6c 75b233aa image013a0000+0x1014
003cfd78 77589ef2 kernel32!BaseThreadInitThunk+0xe
003cfdb8 77589ec5 ntdll!__RtlUserThreadStart+0x70
003cfdd0 00000000 ntdll!_RtlUserThreadStart+0x1b
El desmontaje en la ubicación en image0...
se ve así:
013a100e ff1500713a01 call dword ptr [image013a0000+0x7100 (013a7100)]
013a1014 33c0 xor eax,eax
013a1016 c21000 ret 10h
¡Ahora estamos llegando a alguna parte! En esa dirección encontrarás algunos datos interesantes:
db 013a7100
013a7100 3f fd ba 76 00 00 00
Curiosamente, ahí está nuestra dirección! 76bafd3f
en buena vieja endian pequeña. Retrocedamos un poco:
db 013a7098
013a7098 a0 22 57 77 60 22 57 77-c9 14 b2 75 ff 10 b2 75 ."Ww'"Ww...u...u
013a70a8 7b 44 b2 75 9c 17 b2 75-91 d1 b4 75 71 51 b2 75 {D.u...u...uqQ.u
013a70b8 45 49 b2 75 c4 d1 b4 75-13 49 b2 75 b3 d1 b4 75 EI.u...u.I.u...u
013a70c8 46 e0 57 77 f1 24 59 77-0d 17 b2 75 46 19 b2 75 F.Ww.$Yw...uF..u
013a70d8 2a 30 58 77 61 48 ba 75-83 46 b2 75 07 7c bc 75 *0XwaH.u.F.u.|.u
013a70e8 28 13 b2 75 bf 45 ba 75-ef c7 b3 75 b2 7a b4 75 (..u.E.u...u.z.u
013a70f8 10 14 b2 75 00 00 00 00-3f fd ba 76 00 00 00 00 ...u....?..v....
Veo punteros!
Este es el IAT que encontramos anteriormente, o al menos una matriz de IMAGE_IMPORT_DESCRIPTOR
thunks.
Habiendo llegado tan lejos, ¿qué queda? Bueno, tendríamos que buscar en la memoria volcada para intentar encontrar funciones que coincidan con las DLL conocidas de Windows. Desde allí, podemos comenzar a reconstruir un IAT completo para nuestro nuevo archivo PE. Sospecho que el proceso ImpRec pasa, como se describe en la respuesta de Viv es intentar encontrar estas entradas como lo he hecho a mano. He optado por utilizar puntos de interrupción, pero me imagino que hay varias formas en que esto podría automatizarse de manera diferente. Nunca he escrito una herramienta automatizada para pasar el proceso a mano ...! Dicho esto, en una conjetura adivinando las instrucciones call ptr
, mirar sus entradas fuera de una región asignada y luego tratar de encontrarlas contra las compensaciones de DLL conocidas funcionaría.
Dos notas:
- Escribí esta respuesta como un "así es como lo atacaría manualmente". Escriba la respuesta, con un fondo relevante, no un "uso de esta herramienta".
- Cuidado con ASLR. Esas direcciones de los módulos seguirán cambiando, así que esté atento a cambiar las bases de los módulos.