m0unt41n - whiteboard
Walkthrough and solution for the m0unt41n challenge 'whiteboard', covering an unsigned integer overflow that allows access to a hidden note.
Overview
This m0unt41n challenge provides a small note-taking application written in C. Users can create and view notes by ID. At first glance, the program prevents access to internal notes by shifting user-visible note IDs by one.
However, the note ID parsing contains an unsigned integer overflow. By supplying the maximum 32-bit unsigned integer, the program wraps the calculated note index back to 0, allowing access to the hidden secret note.
Step 1: Application Behavior
The program offers a simple menu:
1
2
3
1. create note
2. view note
3. exit
A secret note is added internally when the program starts:
1
2
3
4
5
6
7
8
9
10
11
12
004012a3 int64_t add_secret_note()
004012a3 {
004012a3 __builtin_strcpy(¬es, "secret\n");
004012d4 FILE* fp = fopen("secret.txt", "r");
004012d4
004012e2 if (!fp)
0040130b return perror("Error opening secret file please contact the organizers");
0040130b
0040130b fgets(&data_404160, 0x100, fp);
00401317 return fclose(fp);
004012a3 }
This means index 0 contains the flag or secret content.
Normal user-created notes are not stored at index 0. Instead, the program increments note_count first and then writes to that index:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
004014f2 int64_t view_note()
004014f2 {
004014f2 printf("id: ");
0040151c int32_t note_count = number_input() + 1;
0040151c
00401526 if (note_count < 0)
00401542 return puts("What did you expect would happen? Maybe try to search stuff that "
00401542 "actually exists next time.\n");
00401542
00401542 if (note_count > note_count)
0040155f return puts("After countless hours of searching your desired note still couldn't "
0040155f "be located. Consider chosing another next time.\n");
0040155f
0040155f puts(&data_40267d);
0040158c printf("\x1b[1m%s\x1b[0m", &(¬es)[(int64_t)note_count * 64]);
004015ae return puts(((int64_t)note_count << 9) + &data_404160);
004014f2 }
So the first user note is stored internally as:
1
user-visible ID 0 -> notes[1]
This is meant to hide index 0 from normal users.
Step 2: Inspecting the Note Lookup
When viewing a note, the program reads the user-provided ID and adds 1:
1
int32_t note_count = number_input() + 1;
The idea is simple:
1
2
3
input 0 -> notes[1]
input 1 -> notes[2]
input 2 -> notes[3]
After that, the program performs bounds checks:
1
2
3
4
5
6
7
00401526 if (note_count < 0)
00401542 return puts("What did you expect would happen? Maybe try to search stuff that "
00401542 "actually exists next time.\n");
00401542
00401542 if (note_count > note_count)
0040155f return puts("After countless hours of searching your desired note still couldn't "
0040155f "be located. Consider chosing another next time.\n");
Finally, it prints the selected note:
1
2
3
0040155f puts(&data_40267d);
0040158c printf("\x1b[1m%s\x1b[0m", &(¬es)[(int64_t)note_count * 64]);
004015ae return puts(((int64_t)note_count << 9) + &data_404160);
At first, this looks like notes[0] should not be reachable, because user input is always shifted by +1.
Step 3: The Vulnerable Input Function
The bug is in the input handling:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
0040131e uint64_t number_input()
0040131e {
0040131e void* fsbase;
0040132a int64_t rax = *(uint64_t*)((char*)fsbase + 40);
0040134c char input_str[0x38];
0040134c fgets(&input_str, 50, stdin);
00401365 uint64_t result;
00401365
00401365 if (!strchr(&input_str, '-'))
0040138e result = strtoul(&input_str, nullptr, '\n');
00401365 else
00401365 {
00401371 puts("why should there be a note with a negative id? Let's just take the first note.\n");
00401376 result = 0;
00401365 }
00401365
00401397 *(uint64_t*)((char*)fsbase + 40);
00401397
004013a0 if (rax == *(uint64_t*)((char*)fsbase + 40))
004013a8 return result;
004013a8
004013a2 __stack_chk_fail();
004013a2 /* no return */
0040131e }
}
The function returns an unsigned int.
On most systems, an unsigned int is 32 bits wide. Its maximum value is:
1
4294967295
In hexadecimal:
1
0xffffffff
The important part is that unsigned integer arithmetic wraps around when the value becomes too large.
So this calculation:
1
4294967295u + 1
does not become 4294967296.
Instead, it wraps around to:
1
0
Or in hexadecimal:
1
0xffffffff + 1 = 0x00000000
Step 4: Triggering the Overflow
To exploit the bug, I selected the option to view a note and entered the maximum unsigned 32-bit integer:
1
2
2
4294967295
The program then performs this calculation:
1
int32_t note_count = number_input() + 1;
Internally, this becomes:
1
2
3
number_input() = 4294967295
4294967295 + 1 = 0
note_count = 0
The bounds checks do not block this:
1
2
note_count < 0 false
note_count > count false
Since note_count is now 0, the program prints notes[0].
Step 5: Leaking the Secret Note
Running the exploit input leaks the hidden note:
1
2
secret
stairctf{XXXXXXXXXXXXXXXXXXXXXXX}
Vulnerability Summary
The core issue is here:
1
int32_t note_count = number_input() + 1;
The function number_input() returns a uint64_t value parsed by strtoul(). However, the result of number_input() + 1 is stored in a 32-bit signed integer.
With the input 4294967295 or other big inputs which convert to 0xffffffff, the calculation becomes:
4294967295 + 1 = 4294967296 0xffffffff + 1 = 0x100000000
When this value is stored in int32_t, it is truncated to the lower 32 bits:
0x100000000 -> 0x00000000
As a result, the calculated note index becomes 0. This bypasses the intended +1 offset and causes the program to access index 0, where the secret note is stored.
The vulnerability can be described as:
An integer truncation bug in the note ID calculation allows an attacker to force the calculated note index to 0, bypassing the intended offset and exposing the hidden note at index 0.
