Función maliciosa
Función que modifica la dirección de retorno para se ejecute ella de nuevo.
En este ejemplo se trata de hacer algo un poco más complejo jugando con los conocimientos de la M++.
Como ya sabemos, cuando se llama a una función, la ejecución salta a la dirección de memoria
de dicha función y se ejecuta hasta que se encuentra con la instrucción de retorno (RET
).
Es entonces cuando la ejecución vuelve a la siguiente dirección de memoria de la función que la llamó.
Internamente la M++ guarda esta dirección de retorno en la pila (stack) al llamar a la función y la recupera para retornar.
Pero… y si modifico la pila (stack) antes de retornar? 😈
Funciones de ayuda
Necesitaremos realizar restas de numeros de 16 bits, creando la logica de dicha funcionalidad en una función a parte. Estas funciones auxiliares van a operar con los registros RD y RE donde guardaremos direcciones de memoria.
Primero las vamos a ver en java.
java
void TFSUB_RD() {RD = RD - 1;}void TFSUB_RE() {RE = RE - 1;}void TFSUB() {TFSUB_RE(); // siempre restamos 1 al registro REif (RE == 0xFF) {// (*) si el registro RE es 0xFF, sabemos que antes de restar 1 al RE estaba en 0x00// por lo que debemos restar 1 al registro RDTFSUB_RD();}}
(*) Esto en el codigo de la M++ no es tan literal, ya que en vez de comprobar si el registro RE es 0xFF, comprobamos si está activado el flag de carry.
El código en la M++ es:
# util functions
TFSUB CALL TFSUB_RE
BC TFSUBRD # (*)
JMP TFSUBRET
TFSUBRD CALL TFSUB_RD
TFSUBRET RET
# RD = RD - 1
TFSUB_RD MOV RD, AC
SUB 01
MOV AC, RD
RET
# RE = RE - 1
TFSUB_RE MOV RE, AC
SUB 01
MOV AC, RE
RET
Gracias a estas funciones ya podemos realizar restas de 16 bits.
Los pasos a seguir son:
- Encontrar la dirección de memoria de retorno.
- Encontrar la dirección de memoria del que llamó a nuestra función.
- Sobreescribir la dirección de memoria de retorno en la pila (stack) con la dirección de memoria de la función que llamó a nuestra función.
- …creando así una función que decide volver a ejecutarse de nuevo… 😈
Pero…
Y si en vez de hacer todo esto, simplemente llamamos a la función de nuevo?
java
int sumar(int a, int b) {return sumar(a, b); // por ejemplo}
El resultado va a ser “parecido” pero no lo es.
Ya hemos visto que al llamar a una función guardamos la información de retorno en la pila. Si llamamos a la función de nuevo, estaremos acumulando datos en la pila hasta que llegue nos quedemos “sin memoria” o lo que es peor, que la propia pila sobrescriba nuestras instrucciónes en memoria 😯.
De la forma planteada inicialmente no hay anidación ya que todo el rato se sobrescribe la dirección de retorno de la pila.
Código final
INISP EEEE # random far address
CALL TFAUTORECURSIVE
FIN
TFAUTORECURSIVE POP # extract return address
MOV AC, RE
POP
MOV AC, RD
# --
# find caller address
CALL TFSUB
CALL TFSUB
CALL TFSUB
# --
# faking return address
# - creating recursive call
MOV RD, AC
PUSH
MOV RE, AC
PUSH
# --
RET
# util functions
TFSUB CALL TFSUB_RE
BC TFSUBRD
JMP TFSUBRET
TFSUBRD CALL TFSUB_RD
TFSUBRET RET
TFSUB_RD MOV RD, AC
SUB 01
MOV AC, RD
RET
TFSUB_RE MOV RE, AC
SUB 01
MOV AC, RE
RET