PASO 1- OCULTAR EL DEBUGGER
VOB utiliza dos métodos para detectar si está corriendo junto a un debugger. El primero es un simple 'MeltICE' que se
basa en comprobar con CreateFileA si están cargados los drivers del
Sice (\\.\SICE, \\.\SIWVID para Win9x y \\.\NTICE para WinNT). Si el
SoftIce no está cargado el valor esperado es -1 (FFFFFFFFh). El nombre
del fichero es el último parámetro en ser empujado a la pila, por lo
tanto (una vez dentro de la API) lo encontraremos en *(esp+4) (en
*esp estará la dirección de retorno). Gracias a los
breakpoints condicionales y las macros del Sice vamos a poder cambiar lo
que apunta a esp+4:
BPX CreateFileA if *(esp->4+4)=='SICE' || *(esp->4+4)=='SIWV'
do "e *(esp+4) 0;x"
El segundo lo que hace es comprobar que no hallamos puesto ningún
BPX en las APIS que llama el juego. Para ello comprueba que el primer
byte de la API no sea 'CCh' (recordad que en realidad un brekpoint (BPX)
es una INT 3 cuyo opcode es 'CCh'). ¿Cómo rompemos entonces en la API
que queremos? En este caso, muy fácil: con un BPM.
PASO 2 - ENCONTRAR EL OEP (Original Entry Point)
El primer método con el cual lo hallé, fue muy "al tuntún": rompiendo en las
APIs en que muchos
programas llaman al iniciarse y trazando a la "retro". Pero
bueno.....conseguí mi objetivo, hallar el OEP: 0047606F
Y ahora viene lo interesante. Fue de esta forma que viene a
continuación, con la cual hallé la forma de hallar el
"verdadero" Entry Point de cualquier juego protegido con VOB
3. Poned un BPM en esa dirección (recordad poner siempre BPM y no BPX
en zonas de memoria que vayan a ser descomprimidas/desencriptadas),
cerrad el programa y lo volverlo a ejecutar. El SoftIce rompe en el OEP.
Es un buen momento para volcar el fichero :)
:a eip
:0047606F JMP EIP
Le damos al F5 para volver a Windows, abrimos el ProcDump...coño!!!
Dónde está nuestro proceso???
Umm, vamos a investigar dónde puede estar la causa....me huele a que
nuestro querido VOB debe crear un thread que compruebe si hemos
parado en el OEP. Ejecutamos de nuevo nuestro juego, con lo
cual volvemos a romper en el OEP. Borramos este breakpoint y lo
cambiamos por uno de lectura y escritura (RW). F5 again y salta el Sice.
VOB está rellenando de ceros toda la sección .code!!! Será maricón.
Si trazamos esta rutina no llegamos a ninguna conclusión con la cual
evitar esto....Vamos a poner un BPM en CreateThread. "F12"
tras romper en CreateThread y trazando un poco veremos que EAX va
cogiendo forma de nuestro OEP.
(en ese thread vemos un sospechoso WaitForSingleObject....me lo
imaginaba es un pequeño truco de sincronización. Nuestro juego debe
activar algún "flag" que le diga a VOB que hemos pasado por
el OEP. El truco no tiene mayor importancia, si trazamos 3 o 4
instrucciones podremos usar tranquilamente el truquillo del "jmp
eip". Pero todavía es pronto para volcar, las APIS están
rederigidas)
PASO 3 -
CORREGIR LAS LLAMADAS A LAS APIS
Vamos a ver como corregirlas. Nos situamos en el OEP y vemos lo
siguiente:
018F:0047606F 6AFF
PUSH FF
018F:00476071 6840314800
PUSH 00483140
018F:00476076 68D0614700
PUSH 004761D0
018F:0047607B 64A100000000
MOV EAX,FS:[00000000]
018F:00476081 50
PUSH EAX
018F:00476082 64892500000000 MOV FS:[00000000],ESP
018F:00476089 83EC68
SUB ESP,68
018F:0047608C 53
PUSH EBX
018F:0047608D 56
PUSH ESI
018F:0047608E 57
PUSH EDI
018F:0047608F 8965E8
MOV [EBP-18],ESP
018F:00476092 33DB
XOR EBX,EBX
018F:00476094 895DFC
MOV [EBP-04],EBX
018F:00476097 6A02
PUSH 02
018F:00476099 E8CEE53A00
CALL 0082466C <--
PRIMERA
LLAMADA A LA API
018F:0047609E 90
NOP <-- ¿¿??
018F:0047609F 59
POP ECX
¿Qué pinta ese NOP hay? Muy fácil. Las APIS,
lo habitual y más común, es llamarlas mediante un CALL DWORD PTR a la
IAT, que son 2 bytes(FF15) + 4 de offset-dirección a la que llama. Pero
aquí lo que hace VOB es una CALL "NORMAL" a una zona de memoria alojada
por el mismo. Un CALL ocupa 1 byte(E8)+4 bytes que es la
longitud del salto. De ahí el "NOP", de ese byte de menos. Y
esa CALL va a parar a:
PUSH F32B009F ; "valor mágico" usado para desencriptar la API correspondiente
PUSH 007E9ECE ; mete en la pila el offset de la rutina que desencripta la API
RET
; salta a esa dirección
Y la rutina que la desencripta es:
PUSHAD
; guarda todos los registros
MOV EDX,003D9F4C
MOV ESI,[EDX+00449847]
; ESI=IMAGE BASE
MOV ECX,[ESI+3C]
; ECX=110h (ESI+ECX=comienzo de la cabecera PE)
MOV EBX,[ECX+ESI+00000080] ; EBX=RVA de la IAT
MOV EAX,[ESI+ECX+28]
; EAX=EntryPoint (no OEP)
ADD EBX,[ECX+ESI+00000084] ; le añade a EBX el tamaño de la IAT
SUB EAX,01FE6EFE
LEA ESI,[ESP+20]
MOV EDI,[ESI]
; obtiene el "valor mágico"
SUB EBX,66EFCEFE
SUB EDI,EBX
; marea EDI
SUB EDI,EAX
; hasta conseguir
ROR EDI,03
; la dirección
ADD EDI,550BFACF
; de retorno
MOV [ESI],EDI
; mete en la pila la dirección de retorno
POPAD
; restaura todos los registros
RET ; Atentos a este RET porque
también haré mención a él mas adelante
Y el RET nos lleva a:
0177:007F946B
E9EAA98077 JMP
MSVCRT!__set_app_type
Si le damos al F8 saltamos a:
MSVCRT!__set_app_type
0177:78003E5A 8B442404 MOV EAX,[ESP+04]
Y la pregunta es: ¿cómo pillamos a dónde
salta la rutina? Muy fácil calculando la longitud del salto.....lo
primero que vamos a hacer es cambiar ese RET por
un salto a nuestra memoria ADUMP. Y en la
memoria ADUMP ensamblamos esto:
MOV EBX,[ESP] ; Pillamos la dirección a la que saltaría el RET (es decir el
007F946B de arriba)
MOV EDX,[EBX+01] ; En EBX+1 tenemos la longitud del salto, la metemos en EDX
ADD EBX,05 ; La longitud del salto es respecto a la siguiente instrucción al
salto (SALTO LEJANO=5 bytes)
ADD EBX,EDX ; Le añadimos a la instrucción siguiente al salto la longitud del mismo
Bien, si hemos hecho todo correctamente, EBX
debe valer 78003E5A.
Ya tenemos lo
que buscabamos. Ahora tenemos que buscar ese valor en la IAT(en la del
fichero original)
Arrrgggghhhh!!! Si buscamos ese valor no lo encontramos..... pueden
pasar tres cosas:
- Que la IAT esté dañada o encriptada
- Que no esté inicializada
- Simplemente que no exista
Pues está dañada y no inicializada. Vamos a
ver cómo deducir ésto. Ejecutamos de nuevo el juego y rompemos en el
OEP. Usaremos el comando "/DUMP" del IceDump, ya que como
veremos en el fichero volcado VOB daña la cabecera PE.......pero
un momento, vamos a ver primero algo de información del exe:
SECCIÓN |
Virtual
Size |
Virtual
Offset |
Raw
Size |
Raw
Offset |
.code |
3D7000 |
1000 |
3D7000 |
1000 |
.rsrc |
AFC0 |
3D8000 |
AFC0 |
3D8000 |
.idata |
1000 |
3E3000 |
200 |
C000 |
.data |
41000 |
3E4000 |
40778 |
C200 |
También miramos el EntryPoint y vemos que es 003E4000.
Qué casualidad no? Justo el inicio de la última sección.....sacamos
la conclusión de que en esa sección está alojado el código del
packer. Por tanto ese código no nos hará falta en el
"dumpeado". La sección .idata tampoco se va a salvar. En esta
sección está la IAT pero la de VOB, no la del fichero original. Ahora
sí:
INICIO
LONGITUD
/DUMP 400000
3E3000 DUMPED.EXE
"400000" es la IMAGEBASE y 3E3000 el final de
la sección ".rsrc"
Nos llama la atención dos cosas:
- El icono del fichero no se ve
- El fichero a pasado de 533kb a 3980kb
El primero es un "problema" muy común en
ficheros dumpeados. Es debido a que los VirtualOffsets y Raw Offsets no
coinciden. Afortunadamente esto es fácil de arreglar. Abrimos el
fichero dumpeado con PEditor, vamos a Section y le damos a
"DumpFixer". Pero NO! Como os decía más arriba, VOB daña la
cabecera PE en tiempo de ejecución, con lo cual en el volcado la
cabecera está dañada. "No problem again", cogeremos la
cabecera del fichero original. Abrimos el fichero original(america.exe)
y el dumpeado(dumped.exe) con HexWorkShop y pegamos la cabecera del
original en la del dumpeado. Ahora sí le damos a "DumpFixer".
Bien, en la cabecera PE todavía están ".idata" y
".data". Las borramos con PEditor. Nos debe quedar una cosa
así:
SECCIÓN |
Virtual
Size |
Virtual
Offset |
Raw
Size |
Raw
Offset |
.code |
3D7000 |
1000 |
3D7000 |
1000 |
.rsrc |
AFC0 |
3D8000 |
AFC0 |
3D8000 |
Bueno y ahora a lo que ibamos, a por nuestra IAT.
Usaremos la técnica de buscar la cadena "kernel32.dll". ÑASCAS!
No la encuentra :( Pensemos. VOB debe por
fuerza inicializar los imports y cargar las dll's que el juego va a
usar. Y para ello usará LoadLibraryA/GetModuleHandleA. Vamos a poner un
BPM en LoadLibraryA. Ejecutar el proceso. En *(esp+4) tendréis el
nombre de la librería. Las seis primeras llamadas a esta función no
tienen mayor importancia. Pero atentos a la 7ª. F12, trazad un poco
hasta llegar a un POP EDI. Le damos a F8 y vemos como EDI pasa a
valer 487836. Si miramos en esa dirección de memoria:
017F:00487836 00 00
00 00 00 00 00 00-00 00 00 00 00 00
5B 02 ..............[..
017F:00487846 C8 FE EF CC F2 F5 FF F4-EC CB F4 E8 9B 00 5C 01
..............\..
En principio nada significativo, pero si subimos unas
páginas hacia arriba:
017F:00487418 FF FF FF FF
90 A3 47 00 00 00 00 00 98 A3 47 00
......G.......G.
017F:00487428 48 75 08 00
00 00 00 00 00 00 00 00
36 78 08 00 Hu..........6x..
017F:00487438 30 B0 07 00
44 76 08 00 00 00 00 00
00 00 00 00 ...Dv..........
017F:00487448 AC 79 08 00 2C B1 07
00 38 75 08 00
00 00 00 00 .y..,...8u......
017F:00487458 00 00 00 00
E8 79 08 00 20 B0 07 00 18 75 08 00
.....y.. ....u..
017F:00487468 00 00 00 00
00 00 00 00 02 7A 08 00
00 B0 07 00 .........z......
017F:00487478 2C 77 08 00
00 00 00 00 00 00 00 00
34 7A 08 00 ,w..........4z..
017F:00487488 14 B2 07 00
20 75 08 00 00 00 00 00
00 00 00 00 .... u..........
017F:00487498 52 7A 08 00
08 B0 07 00 28 75 08 00
00 00 00 00 Rz......(u......
017F:004874A8 00 00 00 00
72 7A 08 00 10 B0 07 00 30 75 08 00
....rz......0u..
017F:004874B8 00 00 00 00
00 00 00 00 7E 7A 08 00
18 B0 07 00 ........~z......
017F:004874C8 C4 76 08 00
00 00 00 00 00 00 00 00
F4 7C 08 00
.v...........|..
017F:004874D8 AC B1 07 00 9C 76 08 00
00 00 00 00 00 00 00 00
.....v..........
017F:004874E8 AC 7D 08 00 84 B1 07
00 90 75 08 00 00 00 00 00
.}.......u......
017F:004874F8 00 00 00 00
E4 7E 08 00 78 B0 07 00
00 00 00 00
.....~..x.......
017F:00487508 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
................
Bien, este "churro de colores" es nuestra IAT
con sus IMAGE_IMPORT_DESCRIPTOR's(Original
First Thunk, TimeDateStamp y
ForwardChain (habitualmente
0's), Name,
First Thunk) y
finalmente el DESCRIPTOR FINAL
NULO.
Los más avispados sacaréis de aquí mismo el tamaño
de la IAT. Con el SoftIce: ? 487518-487428=F0. (trabajamos
siempre en hexadecimal)
Bueno, vamos a ver esa dirección
de memoria en nuestro fichero dumpeado. Abrimos HexWorkShop y vamos al
offset 87428h (=
487428-ImageBase, recordad que esto es así porque hemos equiparado los
Virtual Offsets con los Raw Offsets). En ese offset:
00087428
48 75 08 00 00 00 00 00 00 00 00 00 36 78 08 00 30 B0 07 00
Vamos al offset 00087836 (que es el "NAME"
del primer IMAGE_IMPORT_DESCRIPTOR)
00
00 00 00 00 00 00 00 00 00 00 00 00 00 5B 02 53 65
. . . . . . . . . . . . . .[.Se
74 57 69
6E 64 6F 77 50 6F 73 00 00 5C 01 47 65 74 57 tWindowPos..\.GetW
Coño! El muy cabrón nos ha borrado el nombre de las
dll's!!! Bueno, no pasa nada ;) recordáis los LoadLibraryA--POP
EDI? Pues se corresponden. Me explico. Tras el POP EDI en EDI
tendréis la dirección donde va el nombre de la librería. Pos ya
está. Cada LoadLibraryA nos apuntamos el nombre de la librería en un
papel (jeje OldSchool), trazamos hasta el POP EDI y nos apuntamos al
lado la dirección que nos da EDI. Nos vamos al fichero dumpeado y las
corregimos. Otra posibilidad es pillarla todavía intacta. Para ello
ponemos un BPM de escritura en la IAT y cuando salte el Sice nos fijamos
donde acaba la rutina y ponemos un breakpoint ahí. Volcamos la IAT y la
pegamos en el fichero que habíamos volcado antes. Pues ya tenemos nuestra IAT corregida, ya sólo nos
queda inicializarla. Cómo la inicializamos? Muy fácil, dejaremos que
sea el propio Windows quien nos haga la "faenita". Cada vez
que ejecutamos un proceso el windoze(CreateProcessA) inicializa la
IAT. Pos ya está. Cargamos el fichero con el Loader del Sice (que
es como si lo cargasemos suspendido). Si nos fijamos en los "First
Thunk" de la tabla de arriba vemos que los pointers a las
importaciones empiezan en 7B000 (47B000 en memoria)
017F:00487468 00 00 00 00
00 00 00 00 02 7A 08 00 00 B0 07 00
.........z......
Bien ahora lo que tenemos que hacer es copiarlos a
nuestra memoria ADump. Para ello usaremos el comando "M" del
Sice. Cogemos una longitud considerable, mejor de más que de menos. Con
1000h tendremos de sobra. Y como destino final la memoria ADump+200h (en
esos primeros 200h bytes meteremos la rutina de corrección de API's). :
m 47B000 L 1000 ADumpAddress+200 Ahora ya podemos
saber donde está el pointer correcta a la API correspondiente.
Arrancamos el fichero original y una vez ya sabemos la dirección de la
API (calculando el salto recordad), codificamos una rutinilla tal como:
mov eax,0047b000 ;
dirección donde empiezan los thunks en nuestro fichero volcado
xor edx,edx
mov edi,ADumpAddress+200
loop_busca:
cmp dword ptr[edi+edx],ebx
; buscamos la API en nuestros thunks
jz encontrado
inc edx ;
incrementa el contador
cmp edx,00001000
jnz loop_busca
int 3 ;
int3/bpint3 si algo falla pararemos aquí
encontrado:
add eax,edx ;
47b000+CONTADOR (EDX)
mov word ptr [ecx],15ff
; ECX=dirección de la llamada a la API a
corregir (FF15=call dword ptr)
mov dword ptr[ecx+02],eax
; EAX=dirección correcta a nuestro thunk
Si trazamos unas cuantas llamadas a las APIS
vemos que el "byte de menos" puede ser tanto un NOP como un
CLD y que puede estar antes o después de la CALL:
0177:00476099 E8CEE53A00
CALL 0082466C ; CALL a VOB
0177:0047609E 90
NOP <---
0177:004760BC E874E53A00
CALL 00824635 ; CALL a VOB
0177:004760C1 FC
CLD <---
0177:004760E8 FC
CLD <---
0177:004760E9 E85DE53A00 CALL 0082464B
; CALL a VOB
0177:00476194 90
NOP <---
0177:00476195 E82EE03A00 CALL 008241C8
; CALL a VOB
mov eax,00401000
; inicio de la sección de código
mov ecx,0047b000 ; VOB mete a excepción de .rsrc todas las secciones, por tanto
; no podemos saber con exactitud donde acaba esta sección. Pero
; como sabemos que los thunks empiezan en 47b000 como mucho
; llegará hasta ahí ;)
loop_busca:
cmp byte ptr [eax],e8 ; E8=OPCODE DE CALL
jz encontrado
incrementa:
inc eax
cmp ecx,eax ; Hemos llegado al final de la sección de código?
jnz loop_busca ; No? Pos sigue buscando... :b
int 3 ; BPINT3 y cuando no
halla más llamadas romperemos aquí
encontrado:
---- AHORA MIRAMOS SI EL "BYTE DE
MENOS" ESTÁ ANTES O DESPUÉS ----
cmp byte ptr [ecx+5],90
; 90 = OPCODE de NOP
jz ya_bien
cmp byte ptr [ecx+5],fc ;
FC = OPCODE de CLD
jz ya_bien
dec ecx ; El
"byte de menos" está antes del "CALL",
decrementamos ECX en 1
ya_bien:
----- AQUÍ CALCULARIAMOS LA LONGITUD DEL SALTO, EVITANDO "TOCAR" ECX QUE ES -----
----- DONDE TENEMOS LA DIRECCION DE LA LLAMADA A VOB -----
(Momento de recordar este fragmento de código a la que saltan todas "las calls de VOB":
0177:0082466C 689F002BF3 PUSH F32B009F
0177:00824671 68CE9E7E00 PUSH 007E9ECE
0177:00824676 C3 RET )
---- SUPONIENDO QUE EAX ES LA DIRECCION A LA QUE NOS LLEVA EL SALTO: ----
cmp byte ptr[eax+6],68
; Chequeamos si está el PUSH
007E9ECE
jnz incrementa
; si no incrementa y sigue buscando
cmp dword ptr [eax+7],CE9E7E00
jnz incrementa
--- AQUÍ YA SABEMOS QUE EL CALL PERTENECE A VOB ---
jmp ecx ; Saltamos a esa dirección para que la rutina VOB nos devuelva
; la API correcta
Como VOB deja "intactos"
los registros, se nos queda en ECX la dirección para corregir a
"FF15" ;)
Más de uno seguro que esto ya está llegando a su fin,
pero estáis equivocados, VOB no tiene reservada alguna sorpresita. Si
trazamos unas cuantas llamadas más a las API's vemos que el RET nos
lleva a:
0177:007F945D EB01
JMP 007F9460
0177:007F9460 55
PUSH EBP <--------
0177:007F9461 EB01
JMP 007F9464
· Ojito
a esto
0177:007F9464 8BEC
MOV EBP,ESP <---
0177:007F9466 E908AA8077 JMP 78003E73
;
MSVCRT!__getmainargs
0177:78003E70 55
PUSH EBP
0177:78003E71 8BEC MOV EBP,ESP
0177:78003E73 8B4518 MOV EAX,[EBP+18]
; <--- Salta directamente
aquí
Ostras.......ejecuta parte del código de la API en su
propio código. De nuevo nuestro ingenio puede más que el de los
programadores de VOB ;)
xor edx,edx
cmp byte ptr [ebx],e9
jz salta_directamente
; si es salto directo a la API (E9) nos vamos
--- CALCULARIAMOS A QUE API NOS LLEVA EL SALTO EN EBX ---
mov al,byte ptr[ebx] ;
pillamos del código de VOB el primer byte de la API
incrementa:
; (que
en este caso sería el "55" de arriba)
inc ebx
cmp byte [ebx],al ;
comprobamos que no se repita ese valor
jnz es_salto_a_API?
inc edx
; si encontramos otra coincidencia
aumentamos nuestro contador
es_salto_a_API?:
cmp byte ptr [ebx],e9 ;
hemos encontrado ya el salto a la API???
jnz incrementa
--- AQUI CALCULARIAMOS A QUE API NOS LLEVA EL SALTO Y... ---
reduce:
dec ebx
; buscamos
cmp byte ptr[ebx],al ;
en la API el primer byte
jnz reduce
cmp edx,0
; se repetía ese valor???
jz salta_directamente
dec edx
; reduce el contador y busca de nuevo ese
valor
jmp reduce
salta_directamente:
Casi, casi pero no......aún nos queda una
cosilla.....VOB también redirige los SALTOS A LAS APIS. Aquí tenéis
un ejemplillo:
0177:004761FE FC
CLD
0177:004761FF E9D9E33A00 JMP 008245DD
De nuevo el "CLD" ese por el byte de menos, en
este caso por un "JMP DWORD PTR" cuyos opcodes son FF25. La
rutina para corregir los saltos a las APIS será igual que la las CALL,
salvo que:
- Logicamente en vez de corregir el NOP/CLD-E8 por FF15 lo sustituiremos por FF25
- En vez de buscar los E8 para buscar las llamadas(CALLS) a VOB, buscaremos E9 que es el OPCODE de salto lejano
Bueno pues una vez hallamos corregido todas las llamadas
y saltos a las APIS, volcamos la sección code y la pegamos en el
fichero en el que habíamos reconstruido la IAT. Nos aseguramos de haber
corregido el OEP, el RVA de la IAT y su tamaño, ejecutamos el fichero
y........EYYY FUNCIONA! :)
Pero nuestro fichero reconstruido todavía tiene
"un pero". El fichero ocupa casi 4 megas, mucho si
tenemos en cuenta que el fichero original ocupaba 533 Kb. A qué es
debido? Pues a que hemos volcado con el tamaño del fichero en
memoria(SizeOfImage), que nada tiene que ver con el tamaño del fichero
en disco. Abrimos nuestro fichero con HexWorkShop. Ponemos un poco de
vista y vemos que desde el offset 8B452h hasta el 3D8000h(que es el
principio de la sección .rsrc) son todo 0's. Ahí están esos 3,460,014
bytes "de más" ;) Seleccionamos los primeros 8C000h bytes de
nuestro fichero recontruido y los insertamos en un fichero nuevo.
Después nos vamos al offset 3D8000h, seleccionamos hasta el final y lo
pegamos a continuación en el fichero nuevo que habiamos creado.
Salvamos el fichero y salimos. Umm ahora no se nos ve el iconito del
fichero...esto es debido a que el Raw Offset de la sección .rsrc es
incorrecto y debemos corregirlo con PEditor. También
actualizamos el Raw Size de la sección .CODE que
será "Raw Offset de .rsrc - Raw Offset de .CODE"
(8C000h-1000h=8B000h). Nos debe quedar así:
SECCIÓN |
Virtual
Size |
Virtual
Offset |
Raw
Size |
Raw
Offset |
.code |
3D7000 |
1000 |
8B000 |
1000 |
.rsrc |
AFC0 |
3D8000 |
AFC0 |
8C000 |
Ahora sí........VOB is DEAD! :)
Mr.Ocean [WkT!]
|