This post spoils a CTF challenge … Don’t read if you want to try it !

This CTF was organized by the ANSSI to recruit potential members for the French ECSC CTF team. I managed to finish 7th in the senior category (21-25 years old).

[+] Presentation

Votre objectif : lire le fichier /home/user0/flag.

Connectez-vous avec la commande suivante :

ssh -p 4001 user0@challenges.ecsc-teamfrance.fr

Le mot de passe est user0.

You can download the binary here if you want to try.

[+] Recon

It’s a classical pwn challenge setup. We have a setuid binary, and we need to exploit it in order to read the flag file.

We can retrieve the binary with scp, and create a local exploit !
Let’s run checksec and file to collect basic informations about the binary.

It’s an x86_64 binary, so function arguments are passed through registers and not through the stack. A standard memory address is 64 bits long.
The binary is dynamicaly linked, and his GOT table is not fully read-only, so we may use it in our future exploit. It also has an NX stack, so we can’t use stack shellcodes. As the binary has PIE enabled and server has ASLR enabled, we can’t predict the address of any useful section (code, GOT, heap, stack, libraries …).

After a quick look at disassembly in your favorite tool, it appears that the code is really simple and is vulnerable to a basic stack based buffer overflow.
The reversed pseudo C code is something like that :

int readfile(char *filename,char *file_content)
{
  uint stack_canary;
  FILE *__stream;
  ulong uVar2;
  
  stack_canary = rand();
  stackPush(&canaries,(ulong)stack_canary);
  __stream = fopen(filename,"r");
  if (__stream == NULL) 
  {
    printf("Error opening file \'%s\'",filename);
  }
  else 
  {
    fread(file_content,1,0x7ff,__stream);
    fclose(__stream);
  }
  uVar2 = stackPop(&canaries);
  if ((uint)uVar2 != stack_canary) {
    puts("Stack smashing detected!");
    exit(0xff);
  }
  return uVar2;
}

int main(int argc,char *argv[])
{
  uint __seed;
  int iVar1;
  long lVar2;
  undefined8 *puVar3;
  byte bVar4;
  char file_content [256];
  FILE *local_118;
  char filename [260];
  uint stack_canary;
  
  bVar4 = 0;
  __seed = getpid();
  srand(__seed);
  stack_canary = rand();
  stackPush(&canaries,(ulong)stack_canary);
  lVar2 = 0x100;
  puVar3 = file_content;
 
  if (argc < 2) 
  {
    printf("usage: %s <input_file>\n",**argv[]);
    exit(1);
  }

  local_118 = fopen((char *)*argv[][1],"r");

  if (local_118 == NULL) 
  {
    printf("Unable to open file \'%s\'\n",*argv[][1]);
    exit(2);
  }

  bzero(filename,0x100);
  while( true ) 
  {
    iVar1 = fscanf(local_118,"%s",filename);
    if (iVar1 == -1) break;
    printf("Attempting to read file \'%s\'\n",filename);
    bzero(file_content,0x800);
    readfile(filename,file_content);
    puts(file_content);
    bzero(filename,0x100);
  }
  __seed = stackPop(&canaries);
  if (__seed != stack_canary) 
  {
    puts("Stack smashing detected!");
    exit(0xff);
  }
  return 0;
}

The code iVar1 = fscanf(local_118,"%s",filename); doesn’t check the data size before writing it into memory. Another interesting thing we can notice, is that the binary uses a custom stack canary to prevent stack smashing.

  srand(__seed);
  stack_canary = rand();
  stackPush(&canaries,(ulong)stack_canary)

The canary, which is supposed to be random, is not random at all as it is based on a random number generated with a predictable seed (the PID).

[+] Exploitation

The goal is clear : use the stack overflow to overwrite the EIP-save, and get code execution on return.
In order to do this, we’ll need to leak an address from libc (classical ret2libc) and to find a way to predict the canary to avoid overwriting it with a non valid-value.

The problem is that it requires to interact several times with the program, but it asks us a file to read only one time …
Solution : let’s use a special file type called a ‘named pipe’. This kind of file acts like a standard input/output and can be read by a program and written by another one.
So this pipe will acts like our stdin for this binary : we just need to ask the binary to read our named pipe, and write in this named pipe the name of the file we want it to read.

Using this technique, leaking libc base address is easy : we will just ask the program to read /proc/self/maps, which contains all the memory mappings for the self process (included base address for libraries).
Leaking the canary is easier, as the PID is a known information, we will just need to generate a random value with the PID value as seed, and use it as our canary.

So our exploit will be composed with 3 steps :

You can find the final exploit code here.

[+] Bye

Feel free to tell me what you think about this post :)