Identificación de offsets - Explotación de binarios

Share
Identificación de offsets - Explotación de binarios
audio-thumbnail
The Heathers
0:00
/148.0359

Antes de continuar

El contenido que se muestra en este post es meramente informativo y su misión es servir como apoyo para poder aprender a realizar la identificación de offsets cuando se trata de explotar las fallas de seguridad de binarios. Esta información se distribuye con la intención de servir de aprendizaje para entusiastas de la ciberseguridad, nunca para llevar a cabo prácticas maliciosas.

  • Arquitectura: x86_64

Bienvenidos mis hackers a un nuevo post donde aprenderemos el fundamento de la explotación de binarios, la "identificación de offsets". Los offsets son una cifra esencial cuando hablamos de llevar a cabo lo que se conoce como "Buffer Overflow". Estas técnicas que he mencionado se llevan usando desde hace décadas para hackear aplicaciones y programas de todo tipo que manejaban recursos en memoria, y dependiendo de las protecciones y los recursos del hacker, podían y pueden llegar a ocasionar la toma de control del dispositivo sobre el que se ejecuta dicha aplicación.

Pero no nos apresuremos. Antes de poder llegar a tomar el control de un sistema, debemos primero de saber cómo se realizan estos ataques, y el primer paso para hacer uno nosotros mismos es entender las bases. Por ello vengo a exponer y explicar a todos, cómo identificar un offset.

¿Qué es un offset?

Un offset en el contexto de la explotación de binarios singifica el número de bytes que debe de superarse dentro de un buffer para poder llegar a sobreescribir el puntero de instrucción (rip) de la aplicación.

¿Por qué queremos llegar al puntero de instrucción?

Por que el rip es el encargado de ejecutar las instrucciones en lenguaje ensamblador que prosiguen en la memoria. Si pudieramos sobreescribir el rip de un binario, eso significaría que podríamos tomar control del flujo de ejecución del programa, llegando a poder realizar acciones como "Ejecución de comandos" o "ROP Chains", de la cual he realizado un ejemplo en el siguiente post.

ret2win - ROPEmporium
Third District0:00/274.3771421× Antes de continuar Las prácticas que se realizan en este Captura la bandera (CTF) se trata meramente de prácticas formativas que no deberían nunca ser aplicadas a la vida real con fines maliciosos. Han sido avisados. * Sistema operativo: Debian 13 * Herramientas: pwntools, pwndbg, radare2 Un

Por mirarlo desde un enfoque mucho más metafórico, el offset es el camino hacia la inyección de memoria que contiene el código en lenguaje ensamblador malicioso que haremos ejecutar al programa de forma forzada, evitando que prosiga su ejecución como estaba planeada y pudiendo tomar control del binario.

¿Qué es un buffer?

Un buffer es el espacio en memoria reservado para cierta información (variables). Podríamos decir que el buffer es el espacio que se le asigna a un dato con el que el binario operará. El buffer suele ser de tamaño fijo y si se supera ese límite hasta llegar al rip se produce lo conocido como "Violación de segmento" (SIGSEGV), que significa el cese de ejecución del binario debido a que ha accedido a una zona de memoria sobre la ue no tenía autorización (esto es debido a que se superan los límites del buffer, ha entrado en el rip, el rip ha intentado ejecutar los datos que ha invadido la zona de memoria sobre la que se encontraba debido al desbordamiento de información en el buffer, y al intentar ejecutar la información, vio que no tenía sentido y detuvo la ejecución del programa).

En el ejemplo de abajo, se puede observar de forma má gráfica la representación de un buffer.

#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[]) {
	char message[32];
	strcpy(message, argv[1]);
	printf("%s\n", message);
	return 0;
}

Declaramos una variable de tipo array de caracteres (char []) y el nombre que le ponemos es message. El buffer, que es el tamaño del array (el tamaño que reservamos en memoria) es de 32 bytes, y teniendo en cuenta que un caracter solo ocupa un byte, podemos almacenar en el buffer un total de 32 caracteres. Veamos lo que sucede si ejecutamos el programa y le doy al programa una cadena de caracteres de 13 bytes.

❱❱ ./vuln "Hello World!"
Hello World!

Como era de esperar, el programa realiza su función, que es la siguiente:

Reserva 32 bytes en memoria (Buffer)
                    ↓
Copia la cadena de caracteres del primer argumento del binario al buffer
                    ↓
Accede a la dirección de memoria donde se encuentra la cadena (el buffer)
                    ↓
Usa el contenido del buffer para imprimirlo en pantalla ("Hello World!")

Ahora, veamos que ocurre si me salgo del buffer unos cuantos bytes más.

❱❱ ./vuln "Hellooooooooooooo Wooooooooooooooooorld!"
Hellooooooooooooo Wooooooooooooooooorld!
Violación de segmento

Analizando el binario

Lo que ha sucedido es que he rellenado un buffer de 32 bytes con una cadena de caracteres de 41 bytes, llevando al programa a producir un SIGSEGV. Ahora, echemos un vistazo desde más profundo. Desde la memoria del propio binario usando "PwnDBG".

pwndbg> disas main
Dump of assembler code for function main:
   0x0000000000001149 <+0>:	push   rbp
   0x000000000000114a <+1>:	mov    rbp,rsp
   0x000000000000114d <+4>:	sub    rsp,0x30
   0x0000000000001151 <+8>:	mov    DWORD PTR [rbp-0x24],edi
   0x0000000000001154 <+11>:	mov    QWORD PTR [rbp-0x30],rsi
   0x0000000000001158 <+15>:	mov    rax,QWORD PTR [rbp-0x30]
   0x000000000000115c <+19>:	add    rax,0x8
   0x0000000000001160 <+23>:	mov    rdx,QWORD PTR [rax]
   0x0000000000001163 <+26>:	lea    rax,[rbp-0x20]
   0x0000000000001167 <+30>:	mov    rsi,rdx
   0x000000000000116a <+33>:	mov    rdi,rax
   0x000000000000116d <+36>:	call   0x1030 <strcpy@plt>
   0x0000000000001172 <+41>:	lea    rax,[rbp-0x20]
   0x0000000000001176 <+45>:	mov    rdi,rax
   0x0000000000001179 <+48>:	call   0x1040 <puts@plt>
   0x000000000000117e <+53>:	mov    eax,0x0
   0x0000000000001183 <+58>:	leave
   0x0000000000001184 <+59>:	ret
End of assembler dump.
pwndbg> 

El output anterior nos muestra las instrucciones en ensamblador que ejecuta el binario. Las instrucciones que van desde +0 hasta +4 son lo denominado como el prólogo de procedimiento (encargado de construir el Stack). Las instrucciones desde +8 hasta +11 son los argumentos que recoge el binario, además de las variables de entorno, y las líneas +15 hasta +59 se tratan de las instrucciones que realiza el binario en ensamblador. Para ver de forma más clara cómo se almacena la información en memoria, veamos lo siguiente. Primero, ejecutaremos el programa en GDB usando el comando "run" de la siguiente manera:

pwndbg> run "Hello World!"
Starting program: /home/usuario/daemon/testing/bufferoverflow/vuln "Hello World!"
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Hello World!
[Inferior 1 (process 564284) exited normally]

Como podemos ver, el binario se ejcuta como se esperaba y se guarda la cadena en memoria, y luego, se imprime por pantalla. Ahora, veamos en qué parte de la memoria se guarda, y para ello, estableceremos "breakpoints" que detendrán el programa mientras se está ejecutando. El primero que estableceré será en la línea main+19, que realiza la siguiente instrucción:

0x000000000000115c <+19>:	add    rax,0x8

Añade 8 bytes a rax, pero no sabemos el por qué. Para ello, deberemos averiguar qué hacen las instrucciones anteriores. Ahora sí, estableceré el breakpoint.

pwndbg> break *main+19
Breakpoint 1 at 0x115c: file vuln.c, line 6.
pwndbg> info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000000115c in main at vuln.c:6
pwndbg>

A continuación volveré a ejecutar el binario con la cadena "Hello World!":

pwndbg> run "Hello world!"
Starting program: /home/usuario/daemon/testing/bufferoverflow/vuln "Hello world!"
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, 0x000055555555515c in main (argc=0x2, argv=0x7fffffffdf78) at vuln.c:6
6		strcpy(message, argv[1]);

Como podemos ver en el depurador, nos hemos detenido antes de ejecutar la instrucción add rax,0x8.

pwndbg> disas main
Dump of assembler code for function main:
   0x0000555555555149 <+0>:	push   rbp
   0x000055555555514a <+1>:	mov    rbp,rsp
   0x000055555555514d <+4>:	sub    rsp,0x30
   0x0000555555555151 <+8>:	mov    DWORD PTR [rbp-0x24],edi
   0x0000555555555154 <+11>:	mov    QWORD PTR [rbp-0x30],rsi
   0x0000555555555158 <+15>:	mov    rax,QWORD PTR [rbp-0x30]
=> 0x000055555555515c <+19>:	add    rax,0x8
   0x0000555555555160 <+23>:	mov    rdx,QWORD PTR [rax]
   0x0000555555555163 <+26>:	lea    rax,[rbp-0x20]
   0x0000555555555167 <+30>:	mov    rsi,rdx
   0x000055555555516a <+33>:	mov    rdi,rax
   0x000055555555516d <+36>:	call   0x555555555030 <strcpy@plt>
   0x0000555555555172 <+41>:	lea    rax,[rbp-0x20]
   0x0000555555555176 <+45>:	mov    rdi,rax
   0x0000555555555179 <+48>:	call   0x555555555040 <puts@plt>
   0x000055555555517e <+53>:	mov    eax,0x0
   0x0000555555555183 <+58>:	leave
   0x0000555555555184 <+59>:	ret
End of assembler dump.
pwndbg> 

Si comprobamos las instrucciones anteriores, podemos ver que hay una instrucción que dice: "Guarda la dirección de memoria que esté 30 bytes antes del registro rbp en el registro rax".

mov   rax,QWORD PTR [rbp-0x30]

Pero nosotros no sabemos qué hay dentro de rax, por lo que deberemos de echar un vistazo. Para ello, usaremos el comando x/ en PwnDBG (aunque x/ es originalmente un comando de GDB).

pwndbg> x/xg $rbp-0x30
0x7fffffffde40:	0x00007fffffffdf88

Aparentemente está guardando en rax la dirección de memoria 0x00007fffffffdf88. Continuemos observando las instrucciones adyacentes.

add    rax,0x8

Ahora vemos que a rax se le añaden 8 bytes. Veamos por qué sucede.

pwndbg> info reg rax
rax            0x7fffffffdf88      0x7fffffffdf88

Como podemos ver en el output anterior, aparece que almacena una dirección de memoria. Veamos qué hay dentro de la dirección de memoria.

pwndbg> x/xg 0x7fffffffdf88
0x7fffffffdf88:	0x00007fffffffe2c3

Aparece una nueva dirección de memoria, en este caso, 0x00007fffffffe2c3. Si observamos qué reside dentro de esta nueva dirección de memoria, el programa que se está ejecutando, es decir, el argumento número 0.

pwndbg> x/s 0x00007fffffffe2c3
0x7fffffffe2c3:	"/home/usuario/daemon/testing/bufferoverflow/vuln"

Y si añadimos ocho bytes de memoria a la dirección 0x7fffffffdf88, acabamos en obteniendo como resultado una segunda región de memoria, que es la que almacena el argumento número 1, es decir, la cadena de caracteres.

pwndbg> x/xg 0x7fffffffdf88+8
0x7fffffffdf90:	0x00007fffffffe2f4
pwndbg> x/s 0x00007fffffffe2f4
0x7fffffffe2f4:	"Hello World!"
pwndbg> 

Entendiendo el buffer

Si observamos los bytes que ocupa la cadena con el comando x/32xb, podemos observar que la cadena (Hello World!) ocupa un total de 12 bytes. Sin embargo, a la cadena se le suma el byte 0x00, que es conocido como "Null Terminator", que es el encargado de separar información en la memoria. Sin el Null Terminator, la cadena de caracteres continuaría expandiéndose por la memoria y la cadena no tendría fin, por lo que la longitud total de la cadena es de 13 bytes.

pwndbg> x/32xb 0x00007fffffffe2f4
0x7fffffffe2f4:	0x48	0x65	0x6c	0x6c	0x6f	0x20	0x57	0x6f
0x7fffffffe2fc:	0x72	0x6c	0x64	0x21	0x00	0x53	0x48	0x45
0x7fffffffe304:	0x4c	0x4c	0x3d	0x2f	0x62	0x69	0x6e	0x2f
0x7fffffffe30c:	0x62	0x61	0x73	0x68	0x00	0x57	0x49	0x4e
pwndbg>

Pero, ¿cómo puedo saber que esos 13 bytes pertenecen a las 12 letras de Hello World! mas el Null Terminator? Pues por que esos bytes son representaciones de letras en hexadecimal, tal y como se muestra en esta imagen.

La H sería equivalente a 0x48, la e sería equivalente a 0x65, y demás. De forma que 0x6f57206f6c6c6548 sería Hello Wo y el siguiente octeto de bytes, 0x0021646c72, sería rld!.

A continuación esa cadena se transladará a un espacio de memoria reservado de 32 bytes. He insertado otro breakpoint en la línea en la que define el buffer para que se pueda ver de forma clara.

pwndbg> disas main
Dump of assembler code for function main:
   0x0000555555555149 <+0>:	push   rbp
   0x000055555555514a <+1>:	mov    rbp,rsp
   0x000055555555514d <+4>:	sub    rsp,0x30
   0x0000555555555151 <+8>:	mov    DWORD PTR [rbp-0x24],edi
   0x0000555555555154 <+11>:	mov    QWORD PTR [rbp-0x30],rsi
   0x0000555555555158 <+15>:	mov    rax,QWORD PTR [rbp-0x30]
   0x000055555555515c <+19>:	add    rax,0x8
   0x0000555555555160 <+23>:	mov    rdx,QWORD PTR [rax]
   0x0000555555555163 <+26>:	lea    rax,[rbp-0x20]
=> 0x0000555555555167 <+30>:	mov    rsi,rdx
   0x000055555555516a <+33>:	mov    rdi,rax
   0x000055555555516d <+36>:	call   0x555555555030 <strcpy@plt>
   0x0000555555555172 <+41>:	lea    rax,[rbp-0x20]
   0x0000555555555176 <+45>:	mov    rdi,rax
   0x0000555555555179 <+48>:	call   0x555555555040 <puts@plt>
   0x000055555555517e <+53>:	mov    eax,0x0
   0x0000555555555183 <+58>:	leave
   0x0000555555555184 <+59>:	ret
End of assembler dump.
pwndbg> x/32xb $rax
0x7fffffffde50:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7fffffffde58:	0x00	0x56	0xfe	0xf7	0xff	0x7f	0x00	0x00
0x7fffffffde60:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7fffffffde68:	0x00	0xdf	0xff	0xff	0xff	0x7f	0x00	0x00
pwndbg> 

El registro rax ahora contiene la ubicación del buffer, y lo que se ejecutará a continuación pegará el contenido de rdx (que ahora contiene la cadena Hello World!) en la ubicación del buffer. Si continuamos la ejecución y volvemos a revisar el buffer, nos encontramos con que, efectivamente, la cadena de caracteres sí ha sido pegada dentro del buffer.

pwndbg> continue
pwndbg> x/32xb $rax
0x7fffffffde50:	0x48	0x65	0x6c	0x6c	0x6f	0x20	0x57	0x6f
0x7fffffffde58:	0x72	0x6c	0x64	0x21	0x00	0x7f	0x00	0x00
0x7fffffffde60:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7fffffffde68:	0x00	0xdf	0xff	0xff	0xff	0x7f	0x00	0x00
pwndbg> x/s $rax
0x7fffffffde50:	"Hello World!"
pwndbg> 

Podemos ver cómo el buffer se rellena si en lugar de especificar una cadena de 13 caracteres ponemos una de 32 caracteres (32 bytes, el mismo tamaño que el buffer).

pwndbg> run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
pwndbg> continue
pwndbg> continue
pwndbg> x/s $rax
0x7fffffffde30:	'A' <repeats 32 times>
pwndbg> x/32xb $rax
0x7fffffffde30:	0x41	0x41	0x41	0x41	0x41	0x41	0x41	0x41
0x7fffffffde38:	0x41	0x41	0x41	0x41	0x41	0x41	0x41	0x41
0x7fffffffde40:	0x41	0x41	0x41	0x41	0x41	0x41	0x41	0x41
0x7fffffffde48:	0x41	0x41	0x41	0x41	0x41	0x41	0x41	0x41
pwndbg> 

Identificación del offset

Ahora que hemos observado cómo se rellena el buffer, hay que explicar qué es el Buffer Overflow y cómo realizar el primer paso para conseguirlo: Identificar el offset.

Identificar el offset es posible gracias a muchos factores, y existen varias maneras de identificar el offset de un binario.

  • Primera opción: Usar las funciones cyclic y cyclic_find en pwntools (o cyclic y cyclic -l en PwnDBG).
  • Segunda opción: Identificar el offset manualmente observando los registros a la hora de desbordar el stack.

Primera opción

Primero generaremos una cadena larga de caracteres que nos asegure desbordar el buffer. Podemos escoger el tamaño que queramos, pero en este caso yo escogeré una cadena de un tamaño de 100 bytes. Para ello, usaré el siguiente comando que genere una cadena con un patrón específico de caracteres que me permita identificar en qué punto se desborda el buffer.

❱❱ pwn cyclic -n 8 100
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa

Ahora esta cadena la pondré en PwnDBG y ejecutaré el binario.

pwndbg> run aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
Starting program: /home/usuario/daemon/testing/bufferoverflow/vuln aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa

Program received signal SIGSEGV, Segmentation fault.

Observando más de cerca los detalles sobre los registros y las instrucciones desensambladas, nos encontramos con lo siguiente.

───────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────
 ► 0x555555555184 <main+59>    ret                                <0x6161616161616166>
    ↓

NOTA: Este valor también se puede obtener de los registros en caso de no encontrarlo en el detalle del crash del programa. Para poder observarlo, simplemente se ha de poner el siguiente comando en PwnDBG y fijarse en el valor que se encuentra guardado dentro de la dirección de memoria que almacena el registro rsp:

pwndbg> info reg
rax            0x0                 0x0
rbx            0x7fffffffdf28      0x7fffffffdf28
rcx            0x0                 0x0
rdx            0x0                 0x0
rsi            0x5555555592a0      0x5555555592a0
rdi            0x7ffff7f887b0      0x7ffff7f887b0
rbp            0x6161616161616165  0x6161616161616165
rsp            0x7fffffffde18      0x7fffffffde18
r8             0x0                 0x0
r9             0x0                 0x0
r10            0x0                 0x0
r11            0x202               0x202
r12            0x0                 0x0
r13            0x7fffffffdf40      0x7fffffffdf40
r14            0x7ffff7ffd000      0x7ffff7ffd000
r15            0x555555557dd8      0x555555557dd8
rip            0x555555555184      0x555555555184 <main+59>
eflags         0x10202             [ IF RF ]
cs             0x33                0x33
ss             0x2b                0x2b
ds             0x0                 0x0
es             0x0                 0x0
fs             0x0                 0x0
gs             0x0                 0x0
fs_base        0x7ffff7d9e740      0x7ffff7d9e740
gs_base        0x0                 0x0
pwndbg> x/xg 0x7fffffffde18
0x7fffffffde18:	0x6161616161616166
pwndbg>

Como se puede ver, es exactamente el patrón que buscamos.

El patrón de bytes 0x6161616161616166 pertenece al los caracteres faaaaaaa. Debido a cómo está diseñado Pwntools, con la f de esa cadena de caracteres es capaz de detectar el byte donde desborda el buffer y con una simple operación matemática, devuelve el offset. Esto se puede lograr con el comando cyclic -n8 -l <bytes>.

pwndbg> cyclic -n8 -l 0x6161616161616166
Finding cyclic pattern of 8 bytes: b'faaaaaaa' (hex: 0x6661616161616161)
Found at offset 40
pwndbg>

En este caso, el offset es el 40.

Segunda opción

La segunda opción es manejar manualmente el valor de bytes que se le da al binario hasta conseguir desbordarlo, y afinar el valor de bytes hasta dar con el número exacto que lleva al offset. A continuación una demostración.

Con el mismo binario, yo quiero lograr desbordarlo, por lo que le inserto una cantidad de bytes que sé que va a provocar el desbordamiento. En este caso, 100 bytes al igual que en el ejemplo anterior (los 100 bytes serán un total de 92 bytes que simbolizan ser la letra "A" y 8 bytes que simbolizan ser la letra "B").

pwndbg> run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB
Starting program: /home/usuario/daemon/testing/bufferoverflow/vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB

Program received signal SIGSEGV, Segmentation fault.

Como he visto que se ha producido el error, puedo observar el detalle del crash para observar qué valor se ha quedado en el registro rsp.

───────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────
 ► 0x555555555184 <main+59>    ret                                <0x4141414141414141>
    ↓

Una buena forma de comprobar el offset es comprobar si los últimos 8 bytes son los asignados a la letra "B". Si es así, la cantidad de bytes de la letra "A" indica el offset. Ahora, viendo que se ha producido una violación de segmento (un crash), probaré a reducir la cantidad de bytes a la mitad para observar si ahora finalmente consigo dar con el offset.

pwndbg> run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB
Starting program: /home/usuario/daemon/testing/bufferoverflow/vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB

Program received signal SIGSEGV, Segmentation fault.

Observando el detalle que aparece en la parte inferior, puedo ver que me estoy acercando a dar con el offset exacto.

───────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────
 ► 0x555555555184 <main+59>    ret                                <0x4242424242424141>
    ↓

Esa cadena que ha quedado en el registro rsp coincide con la cadena de caracteres AABBBBBB, lo que significa que me han sobrado 2 bytes correspondientes a la letra "A". Si en el ejemplo anterior generé la cadena de 50 bytes componiéndola con 42 bytes de letras "A" y 8 bytes de letras "B", y para obtener el offset debo eliminar dos letras "A", eso indicaría que el offset es el número 40 (por que recordemos que el offset es igual a la cantidad de letras "A" que hemos especificado en la cadena).

pwndbg> run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB
Starting program: /home/usuario/daemon/testing/bufferoverflow/vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB

Program received signal SIGSEGV, Segmentation fault.
───────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────
 ► 0x555555555184 <main+59>    ret                                <0x4242424242424242>
    ↓

En el ejemplo anterior, habríamos obtenido el offset. Y finalmente, para comprobar que hemos dado con el offset exacto (por si queda algún atisbo de duda), haremos lo siguiente. Probaremos a ver si se produce una violación de segmento usando 40 bytes, y si se produce, probaremos usando 39. Como dijimos que el offset es la cantidad de bytes que debemos alcanzar hasta poder controlar el rip, y la violación de segmento se produce cuando el rip no encuentra instrucciones que ejecutar, nada mas obtener el offset debería de ocurrir una violación de segmento. Probaremos la teoría con el siguiente comando:

pwndbg> run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Starting program: /home/usuario/daemon/testing/bufferoverflow/vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Program received signal SIGSEGV, Segmentation fault.

Y como vemos, con 40 bytes correspondientes a la letra "A" se produce la violación de segmento (SIGSEGV). Probando con 39 bytes, por otro lado, no provoca una violación de segmento.

pwndbg> run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Starting program: /home/usuario/daemon/testing/bufferoverflow/vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[Inferior 1 (process 487511) exited normally]
pwndbg>

Por lo que efectivamente, el offset es 40. Y de estas dos maneras, hemos podido obtener el offset de un binario y avanzar un paso más en la explotación del mismo hasta lograr obtener una shell o realizar una cadena ROP.

Read more