See the assignment's PDF available on BrightSpace for task descriptions and submission requirements.
Level 9
1) num_numbers is a signed integer, because that's how it's initialized:
int num_numbers = 0;This means you can have num_numbers be negative, so it passed the security check to see if it fits in the buffer[32]. Later after the check, it passes num_numbers in read_input(buffer, num_numbers) -> void read_input(int* buffer, unsigned short size, as an unsigned short ! a type mismatch. So, always positive, and it takes only the lower 16 bits of num_numbers, positive. Then inside the loop, this leads to a buffer overflow. The loop assumes the size var is a number not bigger than 32, and it passed the check. Now is can be any number between 0000 and FFFF, and the loop does scanf on the i-th mem location from the start of the buffer, and reads a digit into that.
for(unsigned short i = 0 ; i < size; ++i) {
scanf("%d", &buffer[i]);
}
2) We need to inject shellcode again similar to lv8, but now the only var that enters scanf, and thus the buffer, is integer numbers, which are than interpreted as 32 bits binary. The code uses a signed number for the security check, andt an unsigned number for the loop. We can use a negative number for num_numbers to pass the security check, and it can be larger than 32 when casted to unsigned short which causes a buffer overflow. We need to somehow encode shellcode with those numbers we pass into scanf. i'll do that with a python script just like lv8, since it's scanf and we can send to stdin of the c binary.
Let's disassemble main(), and try to determine the memory location of the buffer start, it should be referenced somewhere above the read_input() call. 0x90 in decimal is 144 bytes, int is 4 bytes, so there are 36 integers to fill until we reach RBP. Then 8 bytes (so 2 numbers) that overwrite old RBP, and 8 more bytes (2 more numbers) that can overwrite the return addr. So 40 numbers in total.
Plan:
convert shellcode to binary, add NOP's at the start so the amount of bits become divisible by 4 (so we can represent using 32 bits, 4 bytes groups). Store it as integers. Second, we need to add padding to overwrite the stack until we reach the old RBP and ret addr. We can overwrite the old RBp addr too, but for the ret addr, we need to get real address of the buffer, which means it'll execute the shellcode.
Since the loop inside read_input still has to finish, the remaining memory locations should just be filled with zeroes. This does mean we shouldn't go too far too many memory locations over the return address. That'll be slow and maybe it'll crash. So we need to find a negative number for num_numbers whose lower 4 digits are a bit more than 40, and the 32bit signed hex value should be negative.
Let's make the lower 4 digits a bit about than 40, lets do 50, in unsigned short hexadecimal that is: 0x0032. The signed 32bit hex should be negative so we go for: 0xFFFF0032, in decimal that's: -65486
We need to find the real address of the buffer, break after the first scanf inside main after filling in a negative number that caster to unsign sshort will be larger than 32. in the assembly we see we need to break at *0x0000555555555296. Then print $rbp - 0x90 (we estalished thats where the buffer is)
students177@appsec2026:/levels/level9$ gdb /levels/level9/level9 (gdb) unset env LINES (gdb) unset env COLUMNS (gdb) break *0x0000555555555296 (gdb) run Starting program: /levels/level9/level9 process 761337 is executing new program: /levels/level9/level9 Welcome to the magic number calculator How many lucky numbers do you have? -65486 Breakpoint 1, 0x0000555555555296 in main () (gdb) print/x $rbp - 0x90 $1 = 0x7fffffffe8e0
Then
python3 ~/lv9_crack.py > ~/payload_lv9.bin (cat ~/payload_lv9.bin ; sleep 0.1 ; cat - ) | /levels/level9/level9
and we escalate