Análisis Packer 01 - Parte VI
Los stolen bytes
Como su nombre indica, los stolen bytes son bytes faltantes, bytes que han sido emulados por el packer y cuando el packer pasa la ejecución al programa ya no existen. Para este artículo voy a denominar stolen bytes a los bytes que hay entre el OEP y el salto a GetModuleHandleA. Este packer para la emulación de los stolen bytes hace uso de una máquina virtual para emular distintas instrucciones por ejemplo: jmp, call, jne, je etc... Analizando la máquina virtual veremos cuales son e iremos resolviendo todo este misterio.
Normalmente para resolver los stolen bytes se recurre a dos formas:
- Copiando directamente todo la máquina virtual y código ofuscado y pegándolo en el mismo ejecutable
- Desofuscando el código instrucción por instrucción.
Aquí lo voy a hacer de la segunda forma y por supuesto hay que entender cómo trabaja la VM, si no no vale para nada. Verás que lleva mucho trabajo.
Analizando el OEP
Vamos a partir lógicamente desde el OEP del packer y como ya conozco su dirección le voy a poner un HBP para que se detenga ahí:
Por otro lado abriremos otro OllyDBG con el dumpeado (dumpe_.exe) para ir rellenando los bytes que encontremos y yo voy a abrir además un tercer OllyDBG con un Delphi 7 para orientarme. Sé que es un Delphi 7 porque cuando analicé el dumpeado con DeDe, éste me lo dijo. Pero antes de comenzar, vamos a analizar el OEP un poco por encima. Si seguimos con F7 el OEP (la imagen anterior) llegamos rápidamente a una call, pero como el código está ofuscado voy a hacer un trace into y te lo muestro:
01B903DE Main push ebp
01B903DF Main prefix rep:
01B903E4 Main lea ebp,dword ptr ds:[esi+ebx-5C]
01B903E8 Main rol ebp,5
01B903EB Main xor ebp,dword ptr ss:[esp+8]
01B903EF Main mov ebp,43FCF2
01B903F4 Main jmp short 01B903F8
01B903F8 Main xor ebp,FFFFFFBC
01B903FB Main lea ebp,dword ptr ss:[esp+eax+30]
01B903FF Main sub ebp,eax
01B90401 Main lea ebp,dword ptr ss:[ebp+ecx-30]
01B90405 Main jmp short 01B90409
01B90409 Main sub ebp,ecx
01B9040B Main jmp 01B9070F
01B9070F Main add esp,-10
01B90712 Main mov eax,47273A
01B90717 Main sub eax,59
01B9071A Main lea eax,dword ptr ds:[ecx+edx*2+6D]
01B9071E Main sub eax,6D
01B90721 Main lea eax,dword ptr ss:[ebp+esi*2+53]
01B90725 Main lea eax,dword ptr ss:[ebp+ecx+74BB20]
01B9072C Main sub eax,ecx
01B9072E Main sub eax,ebp
01B90730 Main push 1B90A18
01B90735 Main call 02250000 ; Se dirige a la máquina virtual
Como puedes ver es un código ofuscado hasta una call dirigida a 2250000 que a partir de ahora la voy a llamar call VM. Lo primero que voy a hacer es desofuscar el código hasta esa call, para eso uso el fantástico plugin Code Doctor. Mira lo que encuentro:
//Packer
01520000 55 push ebp
01520001 8BEC mov ebp,esp
01520003 83EC 10 sub esp,10
01520006 B8 20BB7400 mov eax,74BB20
0152000B 68 180AB901 push 1B90A18
01520010 E8 EBFFD200 call 02250000
A veces el plugin Code Doctor no lo hace correctamente así que hay que hacerlo a mano, por eso hay que intentar coger códigos pequeños.
Inciso antes de seguir: Efectivamente como acabo de decir no hay que fiarse del plugin Code Doctor al 100% porque puede fallar. Hay que usar Code Doctor, tracear a mano con F7 y tener un Delphi 7 al lado, es un buen consejo. Simplemente quiero comentarte que perdí más de una semana buscando un error en el dumpeado y fue porque dicho plugin se equivocó en el análisis: ¡más de una semana! analizándo errores... horroroso.
Sigo con lo de antes: Me fijo en un Delphi 7 y veo que el OEP es idéntico. Fíjate también el puntero de la INIT TABLE (74BB20), ahora mira en el dumpeado y verás que está correcto. Genial.
Ya podemos escribir en el dumpeado. Me voy a él, al OEP real y voy ensamblando las 4 primeras instrucciones que ya conocemos:
//Dumpeado
0074C768 > $ 55 push ebp
0074C769 . 8BEC mov ebp,esp
0074C76B . 83EC 10 sub esp,10
0074C76E . B8 20BB7400 mov eax,74BB20
0074C773 . F1 int1
0074C774 . EE out dx,al ; I/O command
Ahora nos encontramos con esto:
//Packer
01B90730 Main push 1B90A18
01B90735 Main call 02250000 ; Se dirige a la máquina virtual
La máquina virtual emula aquí una instrucción tipo call, jmp, j(condicional). Si observas cualquier Delphi 7 verás que ahí va una call.
Antes de entrar a analizarla, la paso y examino el siguiente código hasta otro nuevo call a la VM. El siguiente código estará lógicamente en 1B90A18 aunque la dirección que verás ahora es otra porque uso Code Doctor:
//Esta dirección 0152004B la crea Code Doctor
0152004B 8D5435 39 lea edx,dword ptr ss:[ebp+esi+39]
0152004F 8D5410 C7 lea edx,dword ptr ds:[eax+edx-39]
01520053 2BD0 sub edx,eax
01520055 8D9439 B0884600 lea edx,dword ptr ds:[ecx+edi+4688B0]
0152005C 2BD7 sub edx,edi
0152005E 8D95 FCC77400 lea edx,dword ptr ss:[ebp+74C7FC]
01520064 2BD5 sub edx,ebp
01520066 68 4204B901 push 1B90442
0152006B E8 90FFD200 call 02250000
Un poco dificil de digerir, así que uso Code Doctor y...
//Esta dirección 0152004B la crea Code Doctor
0152004B 8D15 FCC77400 lea edx,dword ptr ds:[74C7FC]
01520066 68 4204B901 push 1B90442
0152006B E8 90FFD200 call 02250000
Lo que quiero mostrar es la forma de ir desofuscando el código, ir de call VM en call VM.
Entrando en la Máquina Virtual
Ahora viene lo más pesado de todo: analizar que hace esa call 2250000. Como dije antes la VM lo que hace es emular instrucciones tipo call, jmp y saltos condicionales. Dependiendo de la instrucción, veremos en memoria un determinado valor que suele ser: 0, 1 y 2. Voy a seguir la primera instrucción de antes, ésta:
//Packer
01B90730 Main push 1B90A18
01B90735 Main call 02250000 ; Se dirige a la máquina virtual
El push es muy importante ya que es la dirección de retorno. Entramos en la call:
//Packer
02250000 /65:EB 01 jmp short 02250004 ; Superfluous prefix
02250003 -|E9 569C23F3 jmp F5489C5E
02250008 2E:EB 01 jmp short 0225000C ; Superfluous prefix
...
//Código ofuscado. Traceo con F7 hasta el subrayado en verde oscuro:
02250145 83EE 78 sub esi,78
02250148 8DB421 F471BB00 lea esi,dword ptr ds:[ecx+BB71F4]
0225014F 2BF1 sub esi,ecx
02250151 FFD6 call near esi ; protegid.00BB71F4
Entro en 00BB71F4:
//Packer
00BB71F4 55 push ebp ; protegid.00436B9E
00BB71F5 8BEC mov ebp,esp
00BB71F7 83C4 F8 add esp,-8
00BB71FA 53 push ebx
00BB71FB 56 push esi
...
//Sigo traceando con F8 hasta:
...
00BB7283 E8 14FAFFFF call 00BB6C9C ; protegid.00BB6C9C
00BB7288 4F dec edi
00BB7289 0373 6C add esi,dword ptr ds:[ebx+6C]
00BB728C 85FF test edi,edi
00BB728E ^ 77 CB ja short 00BB725B ; protegid.00BB725B
00BB7290 68 AC72BB00 push 0BB72AC ; ASCII "111"
00BB7295 E8 2EB4FEFF call 00BA26C8 ; protegid.00BA26C8
00BB729A 5F pop edi
00BB729B 5E pop esi
00BB729C 5B pop ebx
00BB729D 59 pop ecx
00BB729E 59 pop ecx
00BB729F 5D pop ebp
00BB72A0 C2 1400 retn 14
Como puedes ver, es un lugar característico ya que vemos un ASCII "111" que realmente es una ventana de error:
Entro en 00BB6C9C:
//Packer
00BB6C9C 55 push ebp
00BB6C9D 8BEC mov ebp,esp
00BB6C9F 83C4 F4 add esp,-0C
00BB6CA2 53 push ebx
00BB6CA3 56 push esi
00BB6CA4 57 push edi
00BB6CA5 8BF1 mov esi,ecx
00BB6CA7 8955 FC mov dword ptr ss:[ebp-4],edx
00BB6CAA 8945 F8 mov dword ptr ss:[ebp-8],eax
...
//Fíjate en el código que sigue que es donde quería llegar:
00BB6CE6 FFD2 call near edx
00BB6CE8 2C 01 sub al,1 ; al = 1 (significa que emula a una call)
00BB6CEA 72 79 jb short 00BB6D65
00BB6CEC 2C 02 sub al,2
00BB6CEE 72 07 jb short 00BB6CF7
00BB6CF0 74 2F je short 00BB6D21
00BB6CF2 E9 C2000000 jmp 00BB6DB9
00BB6CF7 8B45 F8 mov eax,dword ptr ss:[ebp-8]
00BB6CFA 8B50 68 mov edx,dword ptr ds:[eax+68]
00BB6CFD 8BC2 mov eax,edx
00BB6CFF 03C7 add eax,edi
00BB6D01 83F8 FF cmp eax,-1
00BB6D04 75 10 jnz short 00BB6D16
00BB6D06 8BC2 mov eax,edx
00BB6D08 0345 F4 add eax,dword ptr ss:[ebp-C]
00BB6D0B 8B55 F8 mov edx,dword ptr ss:[ebp-8]
00BB6D0E 0342 10 add eax,dword ptr ds:[edx+10]
00BB6D11 E9 A8000000 jmp 00BB6DBE
00BB6D16 8B55 F8 mov edx,dword ptr ss:[ebp-8]
00BB6D19 0342 18 add eax,dword ptr ds:[edx+18]
00BB6D1C E9 9D000000 jmp 00BB6DBE ; eax = 01B90D22 (dirección donde será emulada)
...
Como he puesto en los comentarios, si paramos en 00BB6CE8 podemos observar el valor de al. Según valga 0, 1, o 2 emulará a una call, jmp o salto condicional. En este caso al = 1 y emula a una call. ¿Por qué a una call? Porque si comparo con un Delphi 7, lo siguiente que aparece es una call. Y ¿dónde se ejecuta esa call? Es sencillo, si paro en 00BB6D1C veré en eax la dirección donde para. Por este motivo pongo un simple Breakpoint en 01B90D22 y pulso F9:
//Packer
01B90D22 53 push ebx
01B90D23 ^ E9 34FFFFFF jmp 01B90C5C
Y aquí estamos de nuevo ante un código ofuscado. Ahora estamos dentro de la call y hay que actuar igual que al principio. Para que no te pierdas voy a poner de nuevo el OEP original:
//Dumpeado
0074C768 > $ 55 push ebp
0074C769 . 8BEC mov ebp,esp
0074C76B . 83EC 10 sub esp,10
0074C76E . B8 20BB7400 mov eax,74BB20
0074C773 . call direccion_desconocida
El call ya lo he explicado y direccion_desconocida es así porque no puedo poner 1B90D22 ya que es una dirección del packer. ¿Cuánto vale direccion_desconocida? Pues tú puedes injertar código donde quieras y emular la función pero yo lo que voy a hacer es comparar el dumpeado con un Delphi 7 para orientarme y veré cuales son los bytes que el packer ha modificado.
Así de este modo, y comparando con un original veo que la direccion_desconocida es muy posible que vaya aquí:
//Dumpeado
00407F82 |. E8 5DFFFFFF call 00407EE4 ; <jmp.&kernel32.TlsGetValue>
00407F87 |. 85C0 test eax,eax
00407F89 |.^ 74 DB je short 00407F66 ; dumpe_03.00407F66
00407F8B \. C3 retn
00407F8C E9 db E9
00407F8D 06 db 06
00407F8E 88 db 88
00407F8F 78 db 78 ; CHAR 'x'
00407F90 01 db 01
00407F91 AE db AE
00407F92 B9 db B9
00407F93 8A db 8A
00407F94 23 db 23 ; CHAR '#'
Tú no hace falta que te compliques tanto, simplemente crea una nueva sección con el programa Add PE bytes y redirige ahí el código. Para llegar ahí me he fijado en muchos detalles y estoy casi convencido que está bien, por lo tanto, ya puedo modificar el OEP original:
//OEP Original - Dumpeado
0074C768 > $ 55 push ebp
0074C769 . 8BEC mov ebp,esp
0074C76B . 83EC 10 sub esp,10
0074C76E . B8 20BB7400 mov eax,74BB20
0074C773 . call 407F8C
Ahora ya puedo analizar la call 407F8C que si recuerdas está en 01B90D22. Este código igualmente llama a la call VM, por lo tanto voy a ir desofuscando código (no voy a poner el código ofuscado que no ayuda en nada):
//Packer
01B90D22 53 push ebx
01B90C5C 8BD8 mov ebx,eax ; protegid.0074BB20
01B90C5E 33C0 xor eax,eax
01B90C60 C705 CCD07400 0000>mov dword ptr ds:[74D0CC],0
01B90C6A 6A 00 push 0
01B90C6C 68 6E0AB901 push 1B90A6E
01B90C71 E8 8AF36B00 call 02250000
Sigo rellenando este código en el dumpeado:
//Dumpeado
00407F8C 53 push ebx
00407F8D 8BD8 mov ebx,eax
00407F8F 33C0 xor eax,eax
00407F91 C705 CCD07400 000000>mov dword ptr ds:[74D0CC],0
00407F9B 6A 00 push 0
00407F9D 90 nop
...
Queda muy poco para llegar a donde pretendo. Sigo analizando la call VM y observo que al = 1 por lo tanto en 407F9D hay una call. Veamos a dónde va...
¿Qué interesante verdad? Va a 407ED4. Se va a ejecutar la primera instrucción en el código del programa. Muchos consideran a este punto como el supuesto OEP pero ahora ya sabes lo que es: es la call a GetModuleHandleA que nos lo indica correctamente el dumpeado. Así que relleno el dumpeado y te muestro todo seguido lo que hemos conseguido hasta ahora:
//OEP Original - Dumpeado
0074C768 > $ 55 push ebp
0074C769 . 8BEC mov ebp,esp
0074C76B . 83EC 10 sub esp,10
0074C76E . B8 20BB7400 mov eax,74BB20
0074C773 . call 407F8C ---------
... |
|
//Dumpeado |
00407F8C 53 push ebx <--------
00407F8D 8BD8 mov ebx,eax
00407F8F 33C0 xor eax,eax
00407F91 C705 CCD07400 000000>mov dword ptr ds:[74D0CC],0
00407F9B 6A 00 push 0
00407F9D E8 32FFFFFF call 00407ED4 ; <jmp.&kernel32.GetModuleHandleA>
00407FA2 90 nop
00407FA3 90 nop
00407FA4 90 nop
00407FA5 90 nop
...
Hasta aquí quería llegar. Realmente esta parte de recomponer los stolen bytes pues es muy trabajosa pero ya he explicado el método a seguir:
- Desofuscar ese código, por ejemplo con el plugin Code Doctor
- No usar sólo el plugin, tracear también con F7 y ten a mano un Delphi 7 que te ayude.
- Para añadir los stolen bytes puedes usar el plugin Multiassembler.
- Analizar las call VM como se ha explicado y observar el retorno de la call.
Reparar estos stolen bytes ha resultado fácil pero todavía quedan por resolver muchos más. Recuerda que hay que tener muchísima precisión. Desofuscar todo este código no es nada sencilllo y es muy fácil equivocarse. Muchas veces para saber exactamente el código tendrás que crear logs con trace into y después desofuscarlos.
Probando el desempacado
Te puedo asegurar que reparar los stolen bytes ha llevado muchísimo trabajo, no puedes equivocarte ni en un solo byte porque luego los errores son incomprensibles. De todos modos es muy interesante porque se aprende muchísimo y es otra forma diferente de reparar los stolen bytes.
Y ahora podrás decir: ¿Ya está todo? Yo pensaba que me tocaría reparar algún pequeño código más pero me encontré con algo muy curioso que no me esperaba: ejecuté el dumpeado y saltó una excepción porque no puede leer una dirección. Esto puede ser muy común y yo pensaba que sería código que me falta por desofuscar pero cuál fue mi sorpresa cuando veo que el packer inicializa variables antes de llegar al OEP. Verás cómo resolví este inconveniente que merece un artículo entero. En la siguiente parte veremos qué código se ejecuta antes del OEP.