UAM: Reverseando la MOVida malagueña

Introducción

Descargamos el binario de la web del reto, lo descomprimimos y comprobamos su hash. Lo primero que nos llama la atención es la información que nos muestra readelf:

readelf -h Thestral_6ee87b9724dcf5c41ebba4cd578841be
ELF Header:
Magic: 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2’s complement, big endian
Version: 1 (current)
OS/ABI: UNIX – System V
ABI Version: 0
Type: <unknown>: 200
Machine: <unknown>: 0x300
Version: 0x1000000
Entry point address: 0x1c830408
Start of program headers: 872415232 (bytes into file)
Start of section headers: 1352047616 (bytes into file)
Flags: 0x0
Size of this header: 13312 (bytes)
Size of program headers: 8192 (bytes)
Number of program headers: 1536
Size of section headers: 10240 (bytes)
Number of section headers: 5120
Section header string table index: 4864
readelf: Warning: The e_shentsize field in the ELF header is larger than the size of an ELF section header
readelf: Error: Reading 52428800 bytes extends past end of file for section headers
readelf: Warning: The e_phentsize field in the ELF header is larger than the size of an ELF program header
readelf: Error: Reading 12582912 bytes extends past end of file for program headers

Estamos ante un binario de 32 bits, formato ELF, y los datos de la cabecera no son correctos. Simplemente viendo los valores en los campos Type y Machine, ya nos da la pista de lo que hay que corregir: el byte que indica la endianness del binario (fijémonos que readelf lo interpreta como big endian, cuando x86 es little endian). Editamos el byte en el offset 0x5 y lo cambiamos de 0x2 a 0x1 (https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header):

00000000: 7f45 4c46 0102 .ELF..

00000000: 7f45 4c46 0101 .ELF..

Con este cambio, readelf ya nos interpreta los bytes de la manera correcta. Ahora podremos usar el binario dentro de una sesión de depuración con gdb:

readelf -h Thestral_6ee87b9724dcf5c41ebba4cd578841be
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2’s complement, little endian
Version: 1 (current)
OS/ABI: UNIX – System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x804831c
Start of program headers: 52 (bytes into file)
Start of section headers: 10262096 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 6
Size of section headers: 40 (bytes)
Number of section headers: 20
Section header string table index: 19

Buscando cadenas

Antes de analizar el binario dentro de r2, ejecutamos una pasada del comando strings para ver que nos encontramos:

UAM{
m0v3
_me_
1n_7
t0_7
h3_h
0gw4
rts_
cR3w
sch0
_:P}
Mmmmm sorry man.. But that’s not the secret 🙁
Yay!! That was the secret! 😀
Incorrect length! 🙁
Do you know what’s the secret? Tell me the secret:
UAM{this_is_not_the_flag_lol_xd_:P}

Aparte de lo que parecería ser una flag falsa, vemos partes de lo que podría ser una flag correcta. Si nos fijamos bien en los primeros caracteres, parecería que nos falta “0l” de “sch00l”. Eso es algo muy fácil de ver; buscamos cadenas con 3 caracteres, a ver si suena la flauta:

UAM{
pUt
m0v3
_me_
1n_7
t0_7
h3_h
0gw4
rts_
cR3w
sch0
_:P}
0l}

Efectivamente, nos aparece “0l}” y la cadena “pUt”. Esto tiene toda la pinta de ser los caracteres de la flag correcta.

Empieza la MOVida

Lanzamos r2 y nos ponemos a analizar el binario. Tras un rato mirando el código desensamblado de algunas de las funciones del programa, ya nos damos cuenta de que algo no cuadra. ¡Todo son MOVs! Encontramos la explicación en las propias cadenas:

/home/asegura/tools/movfuscator/build//crtd.o

Una rápida búsqueda en Google nos muestra que este binario ha sido ofuscado utilizando Movfuscator. Aparentemente, hay un proyecto de un desofuscador, Demovfuscator. A pesar de ser muy tentador usar el desofuscador, dedicaremos un tiempo a entender los principios de Movfuscator leyendo su paper y usándolo para localizar patrones en el código con códigos simples de ejemplo.

Analizando la MOVida y casi muriendo en el acto

Sabemos, tras leer el paper sobre el ofuscador, que lo primero que hace es registrar 2 handlers para las señales SIGILL y SISGEV. Para cambiar el flujo de ejecución dentro del propio binario, movfuscator usa excepciones de tipo “Illegal Instruction”, mientras que para ejecutar instrucciones externas genera excepciones de tipo “Segmentation Fault”. Para seguir el control del flujo de llamadas a funciones externas la cosa es tan sencilla como usar, por ejemplo, strace y alimentar el programa con cualquier entrada:

echo “LALALALA”|strace ./Thestral_6ee87b9724dcf5c41ebba4cd578841be

Nos interesan las señales SISGEV. Observamos esto:

— SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} — ; printf();
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x8), …}) = 0
brk(NULL) = 0x8c8d000
brk(0x8cae000) = 0x8cae000
brk(0x8caf000) = 0x8caf000
write(1, “Do you know what’s the secret? T”…, 52Do you know what’s the secret? Tell me the secret:
) = 52
— SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} — ; fgets();
fstat64(0, {st_mode=S_IFIFO|0600, st_size=0, …}) = 0
read(0, “LALALALA\n”, 4096) = 9
— SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} — ;strlen();
— SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} — ;printf();
write(1, “Incorrect length! :(\n”, 21Incorrect length! 🙁
) = 21
— SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} — ;exit(-1);
exit_group(-1) = ?
+++ exited with 255 +++

Con una entrada de una longitud inferior a la que espera el programa, vemos diferentes llamadas a la libc justo después de cada segmentation fault. Vamos a usar r2 para intentar determinar los diferentes control path del programa dependiendo de su entrada:

Asignando el handler sa_dispatch para saltar a funciones externas.

El código anterior, en el punto de entrada del binario, registra el handler “sa_dispatch” para procesar la señal 0xb (11, SIGSEGV), Segmentation Fault. Echamos un vistazo rápido a dicho método, que no tiene mayor complicación:

Salto al offset indicado en “external” para ejecutar una llamada externa.

Este código recupera el valor real de la pila sobre el registro ESP, y luego salta a la dirección almacenada en “external”, que apuntará a un dirección válida de una función externa al programa (llamadas a la libc). Recordemos que estamos en arquitectura x86, y los parámetros se pasan por la pila. Después de leer sobre movfuscator, sabemos que éste utiliza 2 pilas virtuales (la virtual stack y la virtual discard stack) pero en el caso de las llamadas a la libc, ésta necesitará acceso a la pila real del programa. Por eso se restaura en la primera línea del método “sa_dispatch”.

Seguir, pues, el control del flujo del programa en cuanto a llamadas externas es fácil. Podemos simplemente ver que valores literales se ponen en “external” mediante instrucciones “mov”, y luego identificar dichos offsets con llamadas a la libc. Así tendremos una idea de lo que está haciendo el programa:

Usando r2, podemos ver claramente que estos OFFSETS se corresponden con las funciones de la libc en la @plt siguientes:

Sabiendo esto, ya sabemos que, si introducimos un código con una longitud diferente de la deseada, el flujo del programa será:

0x0804d597: printf(Tell me the secret.) main
0x0804d87f: fgets(secret) main
0x0804daa1: strlen(secret) == <expected_len> ? main
No:
0x0804df84: printf(Longitud Incorrecta) main
0x0804e14c: exit(-1) main ok

Debemos descubrir, primero de todo, la longitud que espera el programa.

Comprobación de la longitud del secreto

Sabemos que movfuscator asigna valores mediante literales. Por ejemplo, una asignación en lenguaje C del tipo:

int a= 0x23;

Se traduce, tras compilar con movfuscator, en:

mov dword [loc.R2], 0x23

Donde R2 es un virtual register de movfuscator (sí, no tan solo tiene 2 pilas virtuales, el muy cabr….). Sabiendo esto, miramos en la función main si hay algún valor literal asignado a R2:

Buscando literales sobre el Virtual Register R2.

Sabemos que la longitud de la cadena debe ser mayor que 0, y probablemente mayor que 1 carácter ;-). Así que el valor 0x23 (35) es un muy buen candidato. Si observamos la flag falsa, tiene precisamente 35 caracteres:

Longitud de la flag falsa, 35 caracteres.

Probamos a alimentar el programa con la flag falsa. De paso, lo ejecutamos dentro de strace para ver las diferencias del flujo de ejecución con respecto al primer intento:

echo “UAM{this_is_not_the_flag_lol_xd_:P}”|strace ./Thestral_6ee87b9724dcf5c41ebba4cd578841be

El resultado, esta vez, nos confirma que 35 es la longitud de la clave (sin incluir el carácter de nueva línea \x0a). Pero también vemos cosas interesantes con strace:

(…)

— SIGILL {si_signo=SIGILL, si_code=ILL_ILLOPN, si_addr=0x80553dc} —
— SIGILL {si_signo=SIGILL, si_code=ILL_ILLOPN, si_addr=0x80553dc} —
— SIGILL {si_signo=SIGILL, si_code=ILL_ILLOPN, si_addr=0x80553dc} —
— SIGILL {si_signo=SIGILL, si_code=ILL_ILLOPN, si_addr=0x80553dc} —
— SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} — ; printf();
write(1, “Mmmmm sorry man.. But that’s not”…, 47Mmmmm sorry man.. But that’s not the secret 🙁
) = 47
— SIGILL {si_signo=SIGILL, si_code=ILL_ILLOPN, si_addr=0x80553dc} —
— SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} — ;exit(1);
exit_group(1) = ?
+++ exited with 1 +++

Hay un montón de SIGILLs. Sabemos, por el paper de movfuscator, que SIGILL se usa para cambiar el control de flujo dentro del binario. Sabemos también que el código debe de estar ejecutando algún tipo de comparación con nuestra entrada y, tal vez, las cadenas que hemos visto con el comando string. Usando el parámetro -i de strace, podemos añadir OFFSETS a cada llamada y, así, podemos verificar un poco más lo que hace el programa con las llamadas externas. Vemos, por ejemplo, que la llamada a exit(1) no la ejecuta main, sino la función _dispatch:

[080553dc] — SIGILL {si_signo=SIGILL, si_code=ILL_ILLOPN, si_addr=0x80553dc} —  ; Salto a dispatch.
[08048898] — SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} — ; exit(1)
[f7fa6069] exit_group(1) = ?
[????????] +++ exited with 1 +++

La llamada a exit(1) cuando la clave no es válda la ejecuta dispatch(), no main().

El secreto

Hacer reversing de esto es un suicidio, pero tiene algo de divertido también. Apoyándome en el propio paper del proyecto y leyendo sobre otros CTFs similares (https://x0r19x91.github.io/blog/2019-03-14-utctf-mov/, https://x0r19x91.github.io/blog/2019-04-09-swamp-future-fun/), se va entendiendo la lógica del ofuscador. Sabemos que SIGILL desvía la ejecución DENTRO del binario. Sabemos que, cuando la longitud de la clave NO es la correcta, tras la llamada a fgets() hay una llamada a strlen() y finalmente una llamada a printf() y a exit(-1). No hay desvío del flujo de ejecución dentro del programa en absoluto (no hay SIGILLs). En cambio, si la longitud es la correcta, sí:

[0804d895] — SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} — ; fgets()
[f7f80069] fstat64(0, {st_mode=S_IFIFO|0600, st_size=0, …}) = 0
[f7f80069] read(0, “UAM{this_is_not_the_flag_lol_xd_”…, 4096) = 36
[0804dab7] — SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} — ; strlen(input);
[080553dc] — SIGILL {si_signo=SIGILL, si_code=ILL_ILLOPN, si_addr=0x80553dc} —

Tras este SIGILL, siguen otros. Esto indica que el código está ejecutando la comparación. Por supuesto, movfuscator tiene su propia ALU implementada mediante porrón de tablas 1D y 2D en memoria. Comparar cadenas se traduce en un puro trámite de comparar 2 valores numéricos, pero estamos en 32 bits. No podemos comparar valores numéricos más gordos que 4 bytes de una sola pasada, hay que “trozeralos”. Por eso las “cadenas” que hemos visto con el comando strings, en realidad, se usan dentro de movfuscator como valores numéricos literales de 4 bytes, y no como direcciones de memoria que apuntan a dichas cadenas. Sabiendo esto, buscamos literales sobre algún registro virtual R* que contenga, por ejemplo, “UAM{“:

Buscamos “UAM{” como literal.

En el OFFSET 0x0804e4a3 tenemos el literal 0x7b4d4155 (recordemos que x86 es little endian). Sabiendo esto, vamos a buscar todos los literales sobre el registro virtual R3:

Todos los literales encontrados con el comando strings, se asignan al registro virtual R3.

r2 nos revela que hay una función dentro del binario, strcmp(), que a priori parecería ser la encargada de comparar nuestra entrada, de algún modo, con los valores anteriores. Las comparaciones y los saltos condicionales en movfuscator implican, por regla general, evaluar alguna expresión, activar el flag virtual ZF y asignar el valor a b0. Dependiendo del valor de b0, se ejecutará o no un salto. Recordemos que, al ser todo instrucciones MOV, se ejecutan todas las instrucciones SIEMPRE, da igual la entrada del programa. Es dependiendo de los valores indicados que se usará la virtual discard stack para que la ejecución no afecte al comportamiento del programa, o la virtual stack, para que sí lo haga. Esto es muy importante a tener en cuenta, de lo contrario uno se vuelve majareta entendiendo porqué ciertos valores acaban repitiéndose cuando intenta analizar de manera dinámica la ejecución del programa XD.

Consideramos que la función strcmp() debe comparar 9 partes de nuestra entrada de 4 bytes cada una. Sabemos que son 9 porque buscamos referencias a ZF en la función main() (que llama a strcmp() para comparar cada parte):

Se realizan un total de 10 comparaciones en main que pueden desviar el flujo del programa.

La primera comprobación es la longitud de la entrada. Por eso vemos 10 referencias a ZF. Para estar seguros de esto, podemos usar gdb para alterar la ejecución del programa incluso si la longitud no es 35:

gdb -nx -q ./Thestral_6ee87b9724dcf5c41ebba4cd578841be
Reading symbols from ./Thestral_6ee87b9724dcf5c41ebba4cd578841be…(no debugging symbols found)…done.
(gdb) handle SIGILL nostop
Signal Stop Print Pass to program Description
SIGILL No Yes Yes Illegal instruction
(gdb) handle SIGSEGV nostop
Signal Stop Print Pass to program Description
SIGSEGV No Yes Yes Segmentation fault
(gdb) b * 0x0804dd18
Breakpoint 1 at 0x804dd18

Desactivamos temporalmente el punto de interrupción y ejecutamos el binario hasta que nos pide la entrada:

(gdb) dis
(gdb) r

Interrumpimos la ejecución con CTRL+c y activamos de nuevo el breakpoint. Continuamos con la ejecución del programa e introducimos una cadena de longitud != 35, por ejemplo: “BIRRA”:

(gdb) en
(gdb) c
Continuing.
BIRRA

Una vez el programa se interrumpa en nuestro breakpoint, alteramos el valor del registro AL por 0x1, desactivamos el punto de interrupción y continuamos la ejecución:

Breakpoint 1, 0x0804dd18 in main ()
(gdb) i r al
al 0x0 0
(gdb) set $al = 1
(gdb) dis
(gdb) c

Y voilà! Hemos demostrado que teníamos razón:

Program received signal SIGSEGV, Segmentation fault.
Mmmmm sorry man.. But that’s not the secret 🙁

Program received signal SIGILL, Illegal instruction.

Program received signal SIGSEGV, Segmentation fault.
[Inferior 1 (process 22936) exited with code 01]

El secreto no tan secreto.

Seguimos reverseando con r2, fijándonos en bloques principales y usando nuestros pobres conocimientos sobre el ofuscador para intentar localizar lugares dentro del código que sean clave. Nos fijamos en la función strcmp(). Sabemos que R3 se usa en main para inicializar todos los valores de 4 bytes identificados como partes del secreto (algunas partes más que deducibles ya). Tras analizar a grosso modo la función strcmp(), vemos que en el OFFSET 0x0804bd98 el valor del registro virtual R3 es guardado en el registro EDX. Usando GDB, escribimos un pequeño script:

set pagination off
handle SIGILL nostop
handle SIGSEGV nostop
shell rm -rf gdb.txt
set logging on
 
break * 0x0804bd9e
  commands
    silent
    i r edx
    c
  end
 
r
set logging off
shell cp gdb.txt gdb-run.txt
# Obtener salida de caracteres comparados con exito:
shell cat gdb-run.txt |grep -v "0x86"|grep -E "^edx"|awk '{ print $2 }'|xargs|xxd -p -r|rev;echo ""
q

Alimentamos el programa con una flag errónea, y:

echo “UAM{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa”|gdb -q -x bp_flag.txt -nx Thestral_6ee87b9724dcf5c41ebba4cd578841be|tail -1
aaaaUAM{UAM{

Probamos con otra flag sin “UAM{“:

echo “AAA{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa”|gdb -q -x bp_flag.txt -nx Thestral_6ee87b9724dcf5c41ebba4cd578841be|tail -1
AAA{

¡Vaya! Tenemos un oráculo. Como a estas alturas ya estaba algo agotado, escribí un sencillo script en bash que se apoyase en el script de gdb para extraer la flag a partir de los literales guardados en R3:

#!/bin/bash
 
# Todas las cadenas obtenidas con strings -3 y con r2 mov~R3:
flag=("UAM{" "pUt\0" "m0v3" "_me_" "1n_7" "t0_7" "h3_h" "0gw4" "rts_" "cR3w" "sch0" "_:P}" "0l}")
 
# GDB script:
gdbs="bp_flag.txt"
 
# Binario:
binary="Thestral_6ee87b9724dcf5c41ebba4cd578841be"
 
# Argumento pasado al programa:
arg=""
 
# Elementos del array flag-1:
flagl=${#flag[@]}
flagle=`expr $flagl - 1`
 
# 35 as de padding:
padding="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
 
# Bucle para ir obteniendo la clave partida en 9 partes:
for i in  ${!flag[@]};
do
	# Elemento anterior concatenado con nuevo elemento:
	argtmp=${arg}${flag[$i]}
	# Padding hasta la longitud de 35 caracteres:
	argpadded=`printf "%s" ${argtmp} "${padding:${#argtmp}}"`
 
	# Lanzamos GDB y guardamos la ultima linea con los elementos
	# del array reflejados:
	echo "Probando $argpadded"
	out=`echo "${argpadded}"|gdb -q -x $gdbs -nx $binary|tail -1`
 
	# Si el elemento añadido "i" esta en la salida junto con bytes
	# de padding "aaa" delante, es un elemento de la flag. En el
	# caso de que ya no hay padding, bastará con saber si la ejec.
	# del binario nos retorna "Yay":
	if [ ${argtmp} == ${argpadded} ]; then
		# Ya no tenemos padding, lanzamos gdb con esta flag
		# y esperamos el "Yay":
		out=`echo "${argpadded}"|gdb -q -x $gdbs -nx $binary|grep "Yay"` &amp;&amp; arg=${argtmp}
	else
		outt=`echo $out|tail -1`
		echo $outt|grep -E "aaa${flag[$i]}" &amp;&amp; arg=${argtmp}
	fi
 
done
 
# Cuando salimos del bucle, mostramos el valor de arg:
echo "La flag es: ${arg}"
 
# Lanzamos el binario con la flag para comprobar:
echo ${arg}|./${binary}
exit $?

Y, ejecutando el script, obtenemos el secreto no tan secreto. De hecho, esto se podría haber resuelto sin ni siquiera analizar el binario mediante un poco de scripting y prueba y error generando las posibles entradas y alimentando el programa directamente, sin leer sobre movfuscator ni hacer reversing:

Apoyándonos en GDB y el breakpoint de strcmp(), obtenemos la flag mediante el oráculo.

El tiempo que tarda en resolver la flag es de unos 8 segundos en mi ordenador, cuando aproximaciones con el ataque de tipo “perf” son mucho más lentas y tediosas.

real 0m8.015s
user 0m4.933s
sys 0m3.399s

La flag es: UAM{m0v3_me_t0_7h3_h0gw4rts_sch00l}.

 

Lazy mode

El siguiente script de Python obtiene la clave correcta sin necesidad de hacer reversing, tan sólo obteniendo las cadenas con strings -3 y probando la flag falsa para saber que la longitud de la flag es de 35 caracteres:

import itertools
from pwn import *
 
# Cadenas obtenidas con strings -3:
flag = [ "pUt","m0v3","_me_","1n_7","t0_7","h3_h","0gw4","rts_","cR3w","sch0" ]
 
# Las 3 cadenas siguientes las dejamos fuera de las combinaciones. 
# UAM{ esta claro que es la primera cadena de la flag.
# La ultima solo puede ser o 0l} o _:P}:
fs = "UAM{"
fe = [ "0l}","_:P}" ]
 
# Longitud de la clave obtenida con R2:
l = 0x23
 
# binario:
binary = "./Thestral_6ee87b9724dcf5c41ebba4cd578841be"
 
# Generamos todas las posibles combinaciones de longitud 35 y la enviamos al
# programa:
for L in range(0, len(flag)+1):
    for subset in itertools.combinations(flag, L):
        f = fs + ''.join(subset)
        # Probar concatenando la parte final:
        for i in fe:
            f+=i
            if len(f)==l:
                # Probamos contra el programa:
                print "Probando: " + f
                r = process(binary)
                r.recvline()
                # Enviamos la flag:
                r.sendline(f)
                # Si tenemos "Yay!!" en la respuesta, esa es la flag:
                if "Yay!!" in r.recvline():
                    print "[*] La flag es: " + f
                    r.close
                    sys.exit(0)
                r.close()

La ejecución del script nos da la flag correcta:

python brute.py SILENT=1
Probando: UAM{m0v3_me_1n_7t0_7h3_h0gw40l}_:P}
Probando: UAM{m0v3_me_1n_7t0_7h3_hrts_0l}_:P}
Probando: UAM{m0v3_me_1n_7t0_7h3_hcR3w0l}_:P}
Probando: UAM{m0v3_me_1n_7t0_7h3_hsch00l}_:P}
Probando: UAM{m0v3_me_1n_7t0_70gw4rts_0l}_:P}
Probando: UAM{m0v3_me_1n_7t0_70gw4cR3w0l}_:P}
Probando: UAM{m0v3_me_1n_7t0_70gw4sch00l}_:P}
Probando: UAM{m0v3_me_1n_7t0_7rts_cR3w0l}_:P}
Probando: UAM{m0v3_me_1n_7t0_7rts_sch00l}_:P}
Probando: UAM{m0v3_me_1n_7t0_7cR3wsch00l}_:P}
Probando: UAM{m0v3_me_1n_7h3_h0gw4rts_0l}_:P}
Probando: UAM{m0v3_me_1n_7h3_h0gw4cR3w0l}_:P}
Probando: UAM{m0v3_me_1n_7h3_h0gw4sch00l}_:P}
Probando: UAM{m0v3_me_1n_7h3_hrts_cR3w0l}_:P}
Probando: UAM{m0v3_me_1n_7h3_hrts_sch00l}_:P}
Probando: UAM{m0v3_me_1n_7h3_hcR3wsch00l}_:P}
Probando: UAM{m0v3_me_1n_70gw4rts_cR3w0l}_:P}
Probando: UAM{m0v3_me_1n_70gw4rts_sch00l}_:P}
Probando: UAM{m0v3_me_1n_70gw4cR3wsch00l}_:P}
Probando: UAM{m0v3_me_1n_7rts_cR3wsch00l}_:P}
Probando: UAM{m0v3_me_t0_7h3_h0gw4rts_0l}_:P}
Probando: UAM{m0v3_me_t0_7h3_h0gw4cR3w0l}_:P}
Probando: UAM{m0v3_me_t0_7h3_h0gw4sch00l}_:P}
Probando: UAM{m0v3_me_t0_7h3_hrts_cR3w0l}_:P}
Probando: UAM{m0v3_me_t0_7h3_hrts_sch00l}_:P}
Probando: UAM{m0v3_me_t0_7h3_hcR3wsch00l}_:P}
Probando: UAM{m0v3_me_t0_70gw4rts_cR3w0l}_:P}
Probando: UAM{m0v3_me_t0_70gw4rts_sch00l}_:P}
Probando: UAM{m0v3_me_t0_70gw4cR3wsch00l}_:P}
Probando: UAM{m0v3_me_t0_7rts_cR3wsch00l}_:P}
Probando: UAM{m0v3_me_h3_h0gw4rts_cR3w0l}_:P}
Probando: UAM{m0v3_me_h3_h0gw4rts_sch00l}_:P}
Probando: UAM{m0v3_me_h3_h0gw4cR3wsch00l}_:P}
Probando: UAM{m0v3_me_h3_hrts_cR3wsch00l}_:P}
Probando: UAM{m0v3_me_0gw4rts_cR3wsch00l}_:P}
Probando: UAM{m0v31n_7t0_7h3_h0gw4rts_0l}_:P}
Probando: UAM{m0v31n_7t0_7h3_h0gw4cR3w0l}_:P}
Probando: UAM{m0v31n_7t0_7h3_h0gw4sch00l}_:P}
Probando: UAM{m0v31n_7t0_7h3_hrts_cR3w0l}_:P}
Probando: UAM{m0v31n_7t0_7h3_hrts_sch00l}_:P}
Probando: UAM{m0v31n_7t0_7h3_hcR3wsch00l}_:P}
Probando: UAM{m0v31n_7t0_70gw4rts_cR3w0l}_:P}
Probando: UAM{m0v31n_7t0_70gw4rts_sch00l}_:P}
Probando: UAM{m0v31n_7t0_70gw4cR3wsch00l}_:P}
Probando: UAM{m0v31n_7t0_7rts_cR3wsch00l}_:P}
Probando: UAM{m0v31n_7h3_h0gw4rts_cR3w0l}_:P}
Probando: UAM{m0v31n_7h3_h0gw4rts_sch00l}_:P}
Probando: UAM{m0v31n_7h3_h0gw4cR3wsch00l}_:P}
Probando: UAM{m0v31n_7h3_hrts_cR3wsch00l}_:P}
Probando: UAM{m0v31n_70gw4rts_cR3wsch00l}_:P}
Probando: UAM{m0v3t0_7h3_h0gw4rts_cR3w0l}_:P}
Probando: UAM{m0v3t0_7h3_h0gw4rts_sch00l}_:P}
Probando: UAM{m0v3t0_7h3_h0gw4cR3wsch00l}_:P}
Probando: UAM{m0v3t0_7h3_hrts_cR3wsch00l}_:P}
Probando: UAM{m0v3t0_70gw4rts_cR3wsch00l}_:P}
Probando: UAM{m0v3h3_h0gw4rts_cR3wsch00l}_:P}
Probando: UAM{_me_1n_7t0_7h3_h0gw4rts_0l}_:P}
Probando: UAM{_me_1n_7t0_7h3_h0gw4cR3w0l}_:P}
Probando: UAM{_me_1n_7t0_7h3_h0gw4sch00l}_:P}
Probando: UAM{_me_1n_7t0_7h3_hrts_cR3w0l}_:P}
Probando: UAM{_me_1n_7t0_7h3_hrts_sch00l}_:P}
Probando: UAM{_me_1n_7t0_7h3_hcR3wsch00l}_:P}
Probando: UAM{_me_1n_7t0_70gw4rts_cR3w0l}_:P}
Probando: UAM{_me_1n_7t0_70gw4rts_sch00l}_:P}
Probando: UAM{_me_1n_7t0_70gw4cR3wsch00l}_:P}
Probando: UAM{_me_1n_7t0_7rts_cR3wsch00l}_:P}
Probando: UAM{_me_1n_7h3_h0gw4rts_cR3w0l}_:P}
Probando: UAM{_me_1n_7h3_h0gw4rts_sch00l}_:P}
Probando: UAM{_me_1n_7h3_h0gw4cR3wsch00l}_:P}
Probando: UAM{_me_1n_7h3_hrts_cR3wsch00l}_:P}
Probando: UAM{_me_1n_70gw4rts_cR3wsch00l}_:P}
Probando: UAM{_me_t0_7h3_h0gw4rts_cR3w0l}_:P}
Probando: UAM{_me_t0_7h3_h0gw4rts_sch00l}_:P}
Probando: UAM{_me_t0_7h3_h0gw4cR3wsch00l}_:P}
Probando: UAM{_me_t0_7h3_hrts_cR3wsch00l}_:P}
Probando: UAM{_me_t0_70gw4rts_cR3wsch00l}_:P}
Probando: UAM{_me_h3_h0gw4rts_cR3wsch00l}_:P}
Probando: UAM{1n_7t0_7h3_h0gw4rts_cR3w0l}_:P}
Probando: UAM{1n_7t0_7h3_h0gw4rts_sch00l}_:P}
Probando: UAM{1n_7t0_7h3_h0gw4cR3wsch00l}_:P}
Probando: UAM{1n_7t0_7h3_hrts_cR3wsch00l}_:P}
Probando: UAM{1n_7t0_70gw4rts_cR3wsch00l}_:P}
Probando: UAM{1n_7h3_h0gw4rts_cR3wsch00l}_:P}
Probando: UAM{t0_7h3_h0gw4rts_cR3wsch00l}_:P}
Probando: UAM{m0v3_me_1n_7t0_7h3_h0gw4rts_0l}
Probando: UAM{m0v3_me_1n_7t0_7h3_h0gw4cR3w0l}
Probando: UAM{m0v3_me_1n_7t0_7h3_h0gw4sch00l}
Probando: UAM{m0v3_me_1n_7t0_7h3_hrts_cR3w0l}
Probando: UAM{m0v3_me_1n_7t0_7h3_hrts_sch00l}
Probando: UAM{m0v3_me_1n_7t0_7h3_hcR3wsch00l}
Probando: UAM{m0v3_me_1n_7t0_70gw4rts_cR3w0l}
Probando: UAM{m0v3_me_1n_7t0_70gw4rts_sch00l}
Probando: UAM{m0v3_me_1n_7t0_70gw4cR3wsch00l}
Probando: UAM{m0v3_me_1n_7t0_7rts_cR3wsch00l}
Probando: UAM{m0v3_me_1n_7h3_h0gw4rts_cR3w0l}
Probando: UAM{m0v3_me_1n_7h3_h0gw4rts_sch00l}
Probando: UAM{m0v3_me_1n_7h3_h0gw4cR3wsch00l}
Probando: UAM{m0v3_me_1n_7h3_hrts_cR3wsch00l}
Probando: UAM{m0v3_me_1n_70gw4rts_cR3wsch00l}
Probando: UAM{m0v3_me_t0_7h3_h0gw4rts_cR3w0l}
Probando: UAM{m0v3_me_t0_7h3_h0gw4rts_sch00l}
[*] La flag es: UAM{m0v3_me_t0_7h3_h0gw4rts_sch00l}

 

@Disbauxes

UAM Futurama 3 parte 2 partial writeup: ROP contra servidor web sin LEAKS

Introducción

Primero de todo, este no es un write-up completo. Describiré mi exploit para lograr la flag en la segunda parte del reto de Futurama 3 de este mes de la UAM. Se considera el reversing del binario “carl” ya completado y las respectivas vulnerabilidades encontradas para provocar el Buffer Overflow.

El servidor web nos muestra una caja de texto donde podemos introducir el parámetro que recogerá el binario vulnerable carl, y su salida estándar se nos mostrará en el textarea de debajo. Siendo un reto llamado “SSRF”, parecería lógico llegar a imprimir la flag en el textarea tras explotar correctamente el BOF encontrado durante la fase de reversing.

No leaks

Este reto se podía resolver de una manera relativamente habitual en los CTFs de exploiting de BOF: primero, leak. Luego, llamada a system. Sin embargo, y parcialmente inspirado por el reto de c0rn0n4con “Twicat”, me puse como objetivo lograr imprimir la flag con una sola ejecución del payload, sin leaks. Reverseando el binario nos encontrábamos claramente con las llamadas dlsym, dlopen y la cadena “system”. Así pues, llegar a ejecutar system(); sin ningún leak era totalmente factible. Además, el binario estaba repleto de llamadas a funciones que son ideales como primitivas write-what-where (sprintf, strcpy, strncpy, strcat…), por lo que todo apuntaba a que debía ser factible lograr mi objetivo. Como principal problema a superar debía considerar 2 cosas:

Dirección donde escribir el comando a ejecutar

Siendo un binario no PIE, las direcciones siempre son las mismas. A simple vista, la dirección 0x604150 parecía una muy buena candidata. De hecho, en el offset 0x00400f6c de carl veíamos el siguiente código:

Snippet de codigo que mueve a RDI el valor 0x604150 y luego hace un jump a rax.

Este código era ideal para cargar la dirección 0x604150 sobre RDI (único argumento de system) y, teniendo en RAX la dirección de system (gracias a dlsym), ejecutar system con el comando deseado.  Sin embargo, tras el salto a RAX (ejecución de system), carl ejecutaría correctamente el comando pero acabaría con un segfault. El servidor web nos devolvería un “Internal Server Error” sin mostrarnos la salida estándar de carl en el textarea, y como mi intención era mostrar la flag directamente, esta vía estaba descartada.

Dirección del comando a ejecutar y primitiva write-what-where

Para lograr escribir el comando deseado sobre una dirección de mi elección dentro de una sección rw- del binario, busqué primero ROP gadgets del estilo mov [registro], registro. Este tipo de gadget me permitiría hacer cosas del estilo:

# mov [ r14 ], r15
payload += p64(_pop_r14_r15_ret)
payload += p64(0x604150)
payload += “ls l/; \x00”
payload += p64(_write_mem_ret)

Sin embargo, no encontré ninguno que me sirviera. Así que descartada esta opción, seguí buscando, y encontré esto en la función fcn.00400ff7:

Una primitiva write-what-where sobre 0x604180 (nibble) + 0x60184 (otro nibble).

Esto me permitía escribir hasta 8 bytes en dos pasadas, aunque debía luego compensar RBP en el stack dos veces. Llegó a funcionar aunque entonces me quedé sin espacio para mis otros ROP gadgets. En aquel momento no me percaté de que dlopen no era necesaria en absoluto, y que llamando a  dlsym directamente pasandole un NULL en rax como handle (gracias @GNLZ) también retornaba correctamente la dirección de system sobre RAX. Con esto, me habría ahorrado los 3 gadgets de la llamada a dlopen y hubiera tenido suficientes gadgets para lograr el objetivo sobreescribiendo mi comando en 0x604180. Y digo 3 y no 5 gadgets porque yo sí me percaté que dlopen sigue funcionando a pesar de tener un valor indeterminado en RSI (flags) ;-).

Así que sólo me quedaba introducir el comando a ejecutar en la pila del programa y buscar la manera de referenciar dicha dirección dinámica (recordemos, ASLR) usando ROP. Lo primero era fácil, aprovechar el “padding”. Rellené mi padding con algunos patterns básicos “abcdef…ABCDEF…0123..” hasta EIP, y lancé mi exploit dentro de gdb, parando la ejecución de mi payload en el primer gadget y observé los offsets respecto de mi padding y el valor de RSP.  Como es lógico, los OFFSET no varían. Por supuesto, por la manera en que se apilan los datos en el stack, era evidente que mi padding estaría cerca de RSP:

Offset respecto del valor de RSP del padding.

Descubrí que podía referenciar mi padding con algunas instrucciones presentes en el propio binario del estilo [ rbp – OFFSET ]. Pero para ello, claro, debía inicializar RBP al valor de RSP. Encontré un gadget ideal para esto:

mov rbp, rsp; mov rdi, rsi; ret;

Lo siguiente era encontrar o bien un ROP, o bien código legítimo del binario, que me permitiera ejecutar un strcpy, strncpy o similar, rellenando los valores adecuados sobre cada uno de los registros mediante referencias del estilo: mov registro [rbp – OFFSET], teniendo en cuenta que OFFSET debía cumplir el requisito de “caer” dentro de mi padding. Lancé la búsqueda de gadgets (sin éxito), así que busqué en el propio código del binario. Necesitaba que, tras la llamada legítima a la función de escritura de memoria, la función regresara (con LEAVE o RET) para no romper mi ROP chain que venía luego. Me encontré con esta joya:

Primitiva write-what-where con referencias al stack y OFFSETS dentro de mi padding.

Todos los OFFSET presentes caían dentro de mi padding. Podía simplemente escribir valores en mi padding y estos de cargarían sobre los registros RAX, RDX y RCX. RDX tendría la longitud del comando. Bastaría con escribir en el OFFSET correcto de mi padding la longitud del comando como p64(len(cmd)). El comando a ejecutar lo escribiría tal cual directamente sobre el padding, en el OFFSET correcto para que RCX cargase bien dicha dirección [ rbp – 0xb0]. Finalmente, RAX debía acabar computando una dirección dentro del binario que tuviera el valor QWORD adecuado (una dirección dentro de una sección rw- donde poder escribir). El valor 0x604150 que he comentado al principio era ideal, pero el código mov rax, qword [rax+0x30] lee un QWORD. Así que busqué el patrón 0x0000000000604150 sin encontrarlo:

gef➤ search-pattern 0x0000000000604150
[+] Searching ‘\x50\x41\x60\x00\x00\x00\x00\x00’ in memory

Seguí buscando, empezando con la .got.plt:

En la dirección 0x603ef8 tenemos el valor QWORD de la .got.plt

Bien, siempre que mi comando sólo fuera como máximo 8 bytes de largo y no sobreescribiera la siguiente dirección, en un principio podía cargar sobre RAX el valor 0x603ef8-0x30 y así acabaría teniendo la dirección 0x603ef8 sobre RAX. Este valor, 0x00603ec8, debía colocarlo en mi padding en el OFFSET adecuado.

Así, con un primer ROP mov rbp, rsp; mov rdi, rsi; ret; tenía acceso a mi padding pero junto con el slash “/”. No podía controlar el “/”, que se convertía en 0x2f como valor numérico sobre RAX, estropeándome el ataque por completo. Así que ejecuté un segundo ROP idéntico para desplazarme 8 bytes dentro de mi padding y poder controlar la totalidad del valor a escribir sobre RAX. En el exploit, podéis ver claramente 7 As de “prepend”, y luego el valor sobre RAX.

El comando

Sin leaks, ya podía escribir cualquier cosa de hasta 8 bytes de longitud en mi padding y obtener RCE en el servidor. Pero tras mi comando, había parte de morralla de la siguiente dirección concatenada con mi string dentro de la .got.plt y system se quejaba. Así que la única solución era reducir la longitud del comando a 7 bytes y añadir “;” al final. Probando contra el servidor de la UAM, se ejecutaba el primer comando, system se quejaba del segundo pero saliendo con un ROP exit(0) el servidor devolvía la salida de carl en el textarea. Para obtener la flag directamente por pantalla no tenía suficientes caracteres, pero gracias a la shell eso da igual. Algo tan simple como cat /f*; era más que suficiente para lograr mi objetivo!

La flag

Tras pulir un poco mi script y lanzarlo, sin leaks y con una sola ejecución, la tan ansiada flag apareció por pantalla. Fijaos en el padding de la siguiente captura; entre las “B”s y las “C”s se puede ver claramente el comando a ejecutar: cat /f*;. Entre las “A”s y las “B”s el valor que se escribirá sobre RAX:

Lanzando el exploit…

… y consiguiendo la FLAG sin LEAKS y en SSRF!

Podéis bajar el exploit AQUÍ.

@socialkas

Defeating an ELF32 binary with absolutely no leaks without using the ret2_dlresolve technique

 The binary

I was presented with an ELF32 binary with the following protections:

ch77 protections

Disassembling the binary with r2, I quickly recognized a classic stack overflow by abusing the call to read:

There’s a buffer overflow in the read function.

read() would read up to 0x64 bytes into the buffer pointed to by *buf, clearly overwriting EBP and RET. Apart from this more than obvious stack overflow, this binary did not have any interesting functions to analyse. As you can imagine, there was no feasible way to leak data, and we had to assume that ASLR on the remote server was enabled. With no puts, write, printf or alike calls within the binary, we were supposed to use the ret2_dlresolve technique. This, of course, would be the only technique giving us a 100% chance of success, no matter which version of the GNU/Libc6 the remote server was running, and without the need of leaking some data from the program (which, in turn, was not an obvious thing to accomplish).

So I read about this technique and wrote the exploit. But then I sort of though: what if I can leak some data anyways? Wouldn’t that be a good way of improving my knowledge about powning, even if my new exploit was obscenely a lot less reliable? I mean, why not?

Write-what-where

Let’s suppose that the binary we were provided with was running on the very same system where it was originally built. I started by reading the data put into the .comment section of the binary by gcc itself:

readelf -p .comment ch77

String dump of section ‘.comment’:
[ 0] GCC: (Ubuntu 4.8.5-4ubuntu8) 4.8.5

A quick search on Google gave me that that version of the compiler shipped with Ubuntu Xenial Xerus (16.04), and the GNU/Libc6 version for this GNU/Distro was 2.23. So I downloaded this version of the library by using the libc database. Well, at this point I knew that in order to leak data, I needed to replace the call to read with a call to, maybe, write. But of course you cannot do that by just overwriting the .got.plt entry for read() with the OFFSET of the write() function. Then I though: but of course you can, as long as that offset fits within one byte! So I checked both OFFSETS:

[*] Libc6 read offset: 0xd4350
[*] Libc6 write offset: 0xd43c0

These OFFSETS where obtained from the  libc6-i386 package on Xenial Xerus; when it came to the libc6:i386 package (after adding the i386 architecture with dpkg –add-architecture i386), I got the following offsets:

[*] Libc6 read offset: 0xd5b00
[*] Libc6 write offset: 0xd5b70

I tried this on a Debian Jessie LTS with libc6 version 2.19 as well and I got the following OFFSETS:

[*] Libc6 read offset: 0xc8810
[*] Libc6 write offset: 0xc8880

So yes; by overwriting the LSB of the .got.plt entry for read() with the right byte offset of write(), I would have a call to write() any time I forced the binary to execute call read@plt. Thanks to the first read call, I had a write-what-where primitive, so overwriting the LSB was a piece of cake:

ROP to overwrite LSB of .got.plt for read() with the LSB of write’s OFFSET.

After that, the leak was easy too: because now read() was in fact a call to write(), all I had to do was to call read@plt yet again, forging write() arguments so that I could get write’s libc6 address leaked into stdout:

ROP to leak to stdout the address of write.

By running the exploit I had so far, my leak was working fine and I could get all the interesting addresses (system(), the string “/bin/sh”) and so on. But I had a new problem now: because the call to read had been overwritten with write(), now I had no way of inputting more data to the program!

Leaking write’s address right off the binary’s memory.

Restoring read()

The total number of bytes read by the call to read() was 0x64. I needed 20 bytes of padding in order to reach EBP and then RET. Which means I was left with a total of 0x64 -0x18 = 0x4c bytes (76 in decimal) for my ROP chain. Because I had already wasted 0x28 bytes to overwrite read@got.plt and then call write to leak its own address to stdout, I was in fact left with 0x24 bytes of a ROP chain. My idea was, of course, to yet again overwrite LSB of read@got.plt but this time with its original value. This would restore the read() function and so I would be able to input a classic ROP-shell attack to spawn a shell and powned the program. But, how?

I started looking for ROP gadgets within the binary. I sort of though that a good way of restoring the original byte would be to XOR it. So I looked for gadgets that would allow me to XOR something with an address of my control. I found these:

ROP gadgets to XOR a value on memory.

The second ROP gadget (skipping the first instruction, push cs), seemed like a good choice. I needed to make sure that EBP held the value read@got.plt-0xe, and CL the right byte (key) used for the XOR operation in order to restore the original LSB of read(). Which byte that should be? Easy; I computed a XOR key like this:

xor_byte_cl = libc6_read_byte ^ libc6_write_byte

I kept looking for ROP gadgets that would allow me to write the XOR key to CL, or ECX, to no avail. Putting the address read@got.plt-0xe into EBP was easy; a pop ebp; ret gadget would do it. I had that ROP, so I focused all my attention to get the XOR key into CL. After a while, I realized the following truth: if you do not have the proper ROP gadget that suits you, go abuse a legitimate call to a function that will do all the dirty job for you. Internally, the parameters passed to a libc6 function by the stack (on 32 bits) will eventually ended up into registers when making a system call (int 0x80). So for the call to write I had the following:

Before making a system call, the parameters are passed into registers. ECX will hold the buffer to output from.

The third parameter to write, i.e., the total number of bytes to write to, would be put into the EDX register. The buffer where to read data from would be put into the ECX register. The file descriptor to output data to would be put into the EBX register and finally EAX will hold the system call id, 0x4, for write. What I found out by debugging my exploit was that the value put into ECX would hold right after calling write. So by forging a new ROP chain after the leak that would make a new call to write with the second parameter set to a valid address within the program’s memory ending with the XOR key, I would have the XOR key in the CL register.

I wrote this ROP chain:

rop_cl_90 = p32(binary_read_plt)
rop_cl_90 += p32(clean_3_args_stack)
rop_cl_90 += p32(0x1) + p32(patch_address) + p32(0x4)

The offset patch_address was computed this way:

patch_address = 0x804a000 + xor_byte_cl

The only problem with this ROP chain was that I was left with a total of 12 bytes. I still needed to put read@got.plt-0xe into EBP by popping off the stack its value. That meant 8 more bytes (4 for the ROP gadget pop ebp; ret) and 4 more bytes for the actual address I wanted: 0x8049ffe. Then, I needed 4 more bytes to call the ROP gadget that would XOR the LSB of read@got.plt. So it was obvious that I could indeed restore read with those 12 left-over bytes of ROP magic, but from that point on I would not be able to call main() again to get another chance of inputting my ROP-shell! Stuck again.

Let’s clean 4 registers instead of 3

I needed to save 4 bytes and that would be all. Instead of using a ROP gadget that would pop 3 registers, I needed one that would pop 4 registers (the three write parameters already on the stack plus a new one, the address I wanted into EBP). For this to work, of course, EBP should be the last register in the ROP gadget sequence of pops. I found that ROP gadget:

0x080484b8: pop ebx; pop esi; pop edi; pop ebp; ret;

So I rewrote my original ROP like this:

The final ROP chain to restore read’s LSB.

I crafted the third parameter of write to be that of a valid memory address within the program (as a matter of fact, the .bss symbol workaround ;-)) because this value would be popped into EDI, and then the ROP gadget I was about to use to XOR the LSB of .got.plt had a second instruction that might segfault the program:

The second instruction could segfault the program if EDI+0xe was pointing to a non-valid memory address.

At the same time, this was a large value. Because it was used first as the third parameter of write, that meant write will read up to 0x804a040 (workaround address within the .bss segment) bytes of data. As long as this was not segfaulting, I did not care because I could just discard any junk coming from the program.

So the last part of my first ROP attack would be to put the desired value of EBP into the stack, call the ROP gadget that would XOR the key with the LSB of read@got.plt and then call back to main in order to get a second chance of inputting my ROP-shell:

The last part of my ROP attack to restore read().

Trying it out

Once my first ROP attack was written, I tried it out by sending my exploit to the program, waiting for the second call to read and then sending it the LSB of the write function. After that, the program should be sending me 4 bytes back, the address of libc6’s write:

exploit = “A”*padding + p32(0xdeadbeef) + rop_overwrite + rop_write + rop_cl_90 + rop_restore_read
r.send(exploit)
r.send(p8(libc6_write_byte))
leaked_write = int(r.recv(4)[::-1].encode(“hex”),16)

After this first attack, performed in a single shot, the program was back to main and thus waiting for me to input more data. This time, my ROP-shell that was finally populated with the right addresses for system() and the string “/bin/sh”:

Sending my ROP-shell and gaining a remote shell.

Once my exploit was completed, I ran it against Xenial Xerus and Debian Jessie LTS with success:

Running my exploit and gaining a shell without ret2_dlresolve.

You can get the binary and my exploit and try it out yourselves. Of course, this exploit is not reliable when you do not know the libc6 version of the remote server. Besides, it only works with libc6 versions where the offset between write and read are less than 0xff bytes. The intended way was to use ret2_dlresolve, which is 100% reliable for this case. But anyway, I think it’s been interesting to pull this off and I tried harder, I think. No doubt there may be some other ROP gadgets or some other possible combinations so that I could do the same thing with even less bytes, I’m sure. So feel free to let me know!