En un grupo de Facebook el cual frecuento, un miembro solicitó ayuda de cómo traducir el siguiente código ensamblador:
Dump of assembler code for function main: 0x0000054d <+0>: lea ecx,[esp+0x4] 0x00000551 <+4>: and esp,0xfffffff0 0x00000554 <+7>: push DWORD PTR [ecx-0x4] 0x00000557 <+10>: push ebp 0x00000558 <+11>: mov ebp,esp 0x0000055a <+13>: push ebx 0x0000055b <+14>: push ecx 0x0000055c <+15>: sub esp,0x10 0x0000055f <+18>: call 0x450 <__x86.get_pc_thunk.bx> 0x00000564 <+23>: add ebx,0x1a9c 0x0000056a <+29>: mov DWORD PTR [ebp-0x10],0x0 0x00000571 <+36>: lea eax,[ebx-0x19a0] ; “3jd9cjfk98hnd” 0x00000577 <+42>: mov DWORD PTR [ebp-0x14],eax 0x0000057a <+45>: sub esp,0xc 0x0000057d <+48>: push DWORD PTR [ebp-0x14] 0x00000580 <+51>: call 0x3e0 <strlen@plt> 0x00000585 <+56>: add esp,0x10 0x00000588 <+59>: mov DWORD PTR [ebp-0x18],eax 0x0000058b <+62>: mov DWORD PTR [ebp-0xc],0x0 0x00000592 <+69>: jmp 0x5ad <main+96> 0x00000594 <+71>: mov edx,DWORD PTR [ebp-0xc] 0x00000597 <+74>: mov eax,DWORD PTR [ebp-0x14] 0x0000059a <+77>: add eax,edx 0x0000059c <+79>: movzx eax,BYTE PTR [eax] 0x0000059f <+82>: movsx eax,al 0x000005a2 <+85>: imul eax,DWORD PTR [ebp-0x18] 0x000005a6 <+89>: add DWORD PTR [ebp-0x10],eax 0x000005a9 <+92>: add DWORD PTR [ebp-0xc],0x1 0x000005ad <+96>: mov eax,DWORD PTR [ebp-0xc] 0x000005b0 <+99>: cmp eax,DWORD PTR [ebp-0x18] 0x000005b3 <+102>: jl 0x594 <main+71> 0x000005b5 <+104>: sub esp,0x8 0x000005b8 <+107>: push DWORD PTR [ebp-0x10] 0x000005bb <+110>: lea eax,[ebx-0x1992] ; “[+] Codigo generado: %i\n” 0x000005c1 <+116>: push eax 0x000005c2 <+117>: call 0x3d0 <printf@plt> 0x000005c7 <+122>: add esp,0x10 0x000005ca <+125>: mov eax,0x0 0x000005cf <+130>: lea esp,[ebp-0x8] 0x000005d2 <+133>: pop ecx 0x000005d3 <+134>: pop ebx 0x000005d4 <+135>: pop ebp 0x000005d5 <+136>: lea esp,[ecx-0x4] 0x000005d8 <+139>: ret End of assembler dump.
Obteniendo información
Podemos observar que es un ELF (Linux) y que al parecer el código fue copiado desde el depurador GDB con el comando:
Otra forma de obtener el código ensamblador de un archivo ELF es por medio del comando:
Se puede destacar que las direcciones no poseen una base (base address), la explicación a esto es que probablemente el ejecutable fue compilado con la protección PIE.
Sin más preámbulos comentemos las instrucciones:
Como es posible observar, lo que hace este programa es: multiplicar cada carácter de la string 3jd9cjfk98hnd por el tamaño de esta misma e ir sumando el resultado.
Programación en MASM
Pongamos lo teórico a lo práctico, para ello realizamos el mismo programa pero en MASM, aun cuando el sistema operativo será distinto, la arquitectura, set de instrucciones y librerías serán similares.
.model flat,stdcall include c:\masm32\include\windows.inc include c:\masm32\include\kernel32.inc includelib c:\masm32\lib\kernel32.lib includelib c:\masm32\lib\msvcrt.lib printf proto C, :VARARG .const .data code db '3jd9cjfk98hnd' fmt_str db '[+] Codigo generado: %i',0ah,0 result dd ? .data? .code main PROC local tam_code:DWORD mov ecx,sizeof code mov tam_code, ecx mov esi,0 bucle: mov eax,0 mov al, code[esi] mul tam_code ; mul eax * tam_code add result,eax ; suma el producto y lo almacena en result inc esi loop bucle invoke printf, offset fmt_str, result invoke ExitProcess,0 main ENDP END main
Ensamblamos y enlazamos (link), para luego depurar el ejecutable y ver el valor que retorna printf():
Dando como resultado el valor: 15015
Programación en C
Una forma de corroborar el resultado obtenido es que, una vez entendida la lógica del programa podemos intentar replicarlo en lenguaje C y ejecutar este dentro de un sistema GNU/Linux:
#include <stdlib.h> #include <string.h> int main(int argc, char *argv[]){ char* code = "3jd9cjfk98hnd"; int resultado = 0; int i; for(i=0; i<strlen(code); i++){ resultado += code[i] * strlen(code); } printf("[+] Codigo generado: %i\n", resultado); }
Compilamos el código y lo ejecutamos:
El código generado es el mismo.
A continuación se lista el contenido de la función main en lenguaje ensamblador:
Dump of assembler code for function main: 0x080483e4 <+0>: push ebp 0x080483e5 <+1>: mov ebp,esp 0x080483e7 <+3>: push edi 0x080483e8 <+4>: push ebx 0x080483e9 <+5>: and esp,0xfffffff0 0x080483ec <+8>: sub esp,0x30 0x080483ef <+11>: mov DWORD PTR [esp+0x2c],0x8048570 0x080483f7 <+19>: mov DWORD PTR [esp+0x24],0x0 0x080483ff <+27>: mov DWORD PTR [esp+0x28],0x0 0x08048407 <+35>: jmp 0x804844d <main+105> 0x08048409 <+37>: mov eax,DWORD PTR [esp+0x28] 0x0804840d <+41>: add eax,DWORD PTR [esp+0x2c] 0x08048411 <+45>: movzx eax,BYTE PTR [eax] 0x08048414 <+48>: movsx ebx,al 0x08048417 <+51>: mov eax,DWORD PTR [esp+0x2c] 0x0804841b <+55>: mov DWORD PTR [esp+0x1c],0xffffffff 0x08048423 <+63>: mov edx,eax 0x08048425 <+65>: mov eax,0x0 0x0804842a <+70>: mov ecx,DWORD PTR [esp+0x1c] 0x0804842e <+74>: mov edi,edx 0x08048430 <+76>: repnz scas al,BYTE PTR es:[edi] 0x08048432 <+78>: mov eax,ecx 0x08048434 <+80>: not eax 0x08048436 <+82>: sub eax,0x1 0x08048439 <+85>: mov edx,ebx 0x0804843b <+87>: imul edx,eax 0x0804843e <+90>: mov eax,DWORD PTR [esp+0x24] 0x08048442 <+94>: add eax,edx 0x08048444 <+96>: mov DWORD PTR [esp+0x24],eax 0x08048448 <+100>: add DWORD PTR [esp+0x28],0x1 0x0804844d <+105>: mov ebx,DWORD PTR [esp+0x28] 0x08048451 <+109>: mov eax,DWORD PTR [esp+0x2c] 0x08048455 <+113>: mov DWORD PTR [esp+0x1c],0xffffffff 0x0804845d <+121>: mov edx,eax 0x0804845f <+123>: mov eax,0x0 0x08048464 <+128>: mov ecx,DWORD PTR [esp+0x1c] 0x08048468 <+132>: mov edi,edx 0x0804846a <+134>: repnz scas al,BYTE PTR es:[edi] 0x0804846c <+136>: mov eax,ecx 0x0804846e <+138>: not eax 0x08048470 <+140>: sub eax,0x1 0x08048473 <+143>: cmp ebx,eax 0x08048475 <+145>: jb 0x8048409 <main+37> 0x08048477 <+147>: mov eax,DWORD PTR [esp+0x24] 0x0804847b <+151>: mov DWORD PTR [esp+0x4],eax 0x0804847f <+155>: mov DWORD PTR [esp],0x804857e 0x08048486 <+162>: call 0x8048300 <printf@plt> 0x0804848b <+167>: lea esp,[ebp-0x8] 0x0804848e <+170>: pop ebx 0x0804848f <+171>: pop edi 0x08048490 <+172>: pop ebp 0x08048491 <+173>: ret End of assembler dump.
El resultado es similar al código publicado por el miembro del grupo, aunque no hay protección PIE (utilicé una versión de gcc más antigua), la cantidad de instrucciones difieren y se utilizan algunas instrucciones distintas como repnz scas y not.
Otra cosa que podemos observar, es la ineficiencia dentro del BUCLE, pues cada vez que pasa por él, obtiene el tamaño de la string:
0x0804846a <+134>: repnz scas al,BYTE PTR es:[edi] 0x0804846c <+136>: mov eax,ecx 0x0804846e <+138>: not eax 0x08048470 <+140>: sub eax,0x1
Por lo general, como buena práctica de programación, se recomienda usar variables en vez de constantes pues facilita futuras modificaciones de código, irónicamente a nivel de lenguaje ensamblador, esta buena práctica (en este caso) hizo al programa menos eficiente.
Se recomienda escribir el FOR de esta manera:
for(i=0; i<13; i++){ resultado += code[i] * strlen(code); }
En vez de:
for(i=0; i<strlen(code); i++){ resultado += code[i] * strlen(code); }
Lo más probable, es que el compilador al momento de leer el código de fuente, encontró que la variable code no era una constante y por ello la necesidad de insertar una instrucción que verificara en cada ciclo del bucle su tamaño por si esta fuese alterada.
Conclusiones
Como conclusión fue posible entender el comportamiento de un binario por medio de la lectura de su código muerto, recreamos el programa tanto en lenguaje ensamblador como en C, logramos visualizar el proceso de transcripción de un lenguaje de alto nivel (conocido también como lenguaje intermedio) y cómo este podría degradar el rendimiento en ciertas circunstancias. Cualquier persona que se está iniciando en programación pensaría que lo mejor sería programar todo en lenguaje ensamblador, pero se debe tener en cuenta que el tiempo necesario para programar es mayor que en los lenguajes de alto nivel, es por ello que se tiende a utilizar este magnifico lenguaje sólo en circunstancias específicas, como por ejemplo: en proyectos en donde lo primordial es la eficiencia y calidad de la programación (micro-controladores, drivers, sistemas embebidos) por sobre el tiempo que esto requiera.
5 Comentarios
Hola,
Se que el post tiene un tiempo, pero a lo mejor puedes ayudarme.
En ese mismo código assembly, ¿Como podria identificar los Basic Blocks?
Muchas gracias
Hola Jesus, para identificar los bloques de código ensamblador, debes separar este código cada vez que encuentres instrucciones de salto (JMP y JB en este caso), saludos
Muchas gracias por responderme 🙂
Indicas JMP y JB. Pero en el código original (el del inicio de la página) ¿habría que tener encuenta el JL? es decir, ¿habría 3 bloques de código?
Gracias de nuevo!
es posible que al ser como dice Jesús el bucle sea un DO-WHILE en vez de un FOR?
Hola Victor,
porque deberiamos utilizar un bucle for y no un DO WHILE?