Código Muerto ASM x86

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:

disas main

Otra forma de obtener el código ensamblador de un archivo ELF es por medio del comando:

objdump -d ./archivo

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.

El código queda de la siguiente forma:
.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.

 

Compartir

5 Comentarios

Jesus 26/09/2019

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

Victor Gutiérrez 26/09/2019

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

Jesus 02/10/2019

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!

Matias 11/09/2020

es posible que al ser como dice Jesús el bucle sea un DO-WHILE en vez de un FOR?

Matias 15/09/2020

Hola Victor,

porque deberiamos utilizar un bucle for y no un DO WHILE?

Agregar un comentario