Post

Bluehens 2024 CTF

XS2: Looper - xor school

image

Get Part of the XOR Key

To kick things off, We leveraged the magic of knowing the flag format udctf{. XORing this snippet against the start of the ciphertext started to reveal a familiar, cheeky pattern in CTFs: deadbe. At this point, the puzzle pieces were coming together.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def xor(msg, key):
    o = b''
    for i in range(len(msg)):
        o += bytes([msg[i] ^ key[i % len(key)]])
    return o

ciphertext_hex = "11010210041e125508065109073a11563b1d51163d16060e54550d19"
ciphertext_bytes = bytes.fromhex(ciphertext_hex)
flagpart = "udctf{"

def find_key(ciphertext, flagpart):
    possible_keys = []
    for i in range(len(ciphertext) - len(flagpart) + 1):
        xor_result = xor(ciphertext[i:i+len(flagpart)], flagpart.encode())
        if xor_result.isalnum():
            possible_keys.append((i, xor_result.decode('utf-8', 'ignore')))
    return possible_keys

keys = find_key(ciphertext_bytes, flagpart)
for idx, key_part in keys:
    print(f"Found key segment at position {idx}: {key_part}")

Then it hit us โ€” the challenge name, โ€œLooper,โ€ was no accident! It hinted that our partial key should loop, leading us to the classic full XOR key: deadbeef. A CTF favorite, deadbeef.

Get the flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def xor(msg, key):
    o = b''
    for i in range(len(msg)):
        o += bytes([msg[i] ^ key[i % len(key)]])
    return o

ciphertext_hex = "11010210041e125508065109073a11563b1d51163d16060e54550d19"
ciphertext_bytes = bytes.fromhex(ciphertext_hex)

# 'deadbeef' -> repeating key
full_key = "deadbeef"
decrypted_message = xor(ciphertext_bytes, full_key.encode())

print(f"Decrypted message: {decrypted_message.decode('utf-8', 'ignore')}")

flag udctf{w3lc0me_t0_x0r_sch00l}

image

Training Problem: Intro to Reverse - training - reversing

We started by analyzing the given binary in IDA Pro, where we noticed a validation check on user input inline to the challenge description

String in Binary: The binary contains a hardcoded string, ucaqbvl,n*d\\'R#!!l, stored in v5. Validation Logic: The program takes input and checks if each character in the input, adjusted by its index (s[i] - i), matches the corresponding character in v5.

Reversing the Check

Using the observed transformation, we wrote a Python script to generate the correct input by reversing the logic.

1
2
3
v5 = "ucaqbvl,n*d\\'R#!!l"
s = ''.join(chr(ord(v5[i]) + i) for i in range(len(v5)))
print(s)

flag: udctf{r3v3ng3_101} image

๐Ÿ…ฑ๏ธrainrot.c - reversing

The challenge, Brainrot, required translating code words from the โ€œbrainrotโ€ format and then reversing the logic to reveal part of the flag. After breaking down the logic and using translation techniques, we were left with a 4-letter word to complete the flag.

Solution

  • Code Translation: We translated the given โ€œbrainrotโ€ words back into their intended text format.
  • Logic Reversal: We reversed the logical checks used in the challenge, piecing together the final portion of the flag.
  • Final Search: A quick Google search confirmed the 4-letter word we needed: ohio.

Translated C 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
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void check_rule(int rule) {
    printf("Flag's a bust, rule %d ain't vibin.\n", rule);
    exit(1);
}

void main() {
    char input[100];
    printf("Enter the flag: ");
    fgets(input, 100, stdin);

int length = strlen(input);
if (length > 0 && input[length - 1] == '\n') {
    input[length - 1] = '\0';
    length -= 1;
}

if (length != 51) check_rule(0);

char prefix[6] = "     ";
strncpy(prefix, input, 5);
if (strcmp(prefix, "udctf") != 0) check_rule(1);

if (input[length - 1] != 0x7d) check_rule(2);

if ((input[5] * 4) % 102 != 'T') check_rule(3);

if ((input[35] | input[33]) != 0x69) check_rule(4);

if (input[6] ^ input[31]) check_rule(5);

if ((input[31] + input[35]) != (input[6] * 2)) check_rule(6);

if ((input[7] == input[10]) + (input[14] == input[23]) + (input[28] == input[36]) != 3) check_rule(7);

if (!((input[42] == input[28]) && (input[36] == input[23]) && (input[10] == input[42]))) check_rule(8);

if (input[10] != 0x5f) check_rule(9);

char fanum[7] = {0x47, 0x4a, 0x13, 0x42, 0x58, 0x57, 0x1b};
char simp[8] = "       ";
char vibe[8] = "       ";
char drip[9] = "        ";

strncpy(simp, input + 29, 7);
strncpy(vibe, input + 43, 7);
strncpy(drip, input + 15, 8);

for (int i = 0; i < 7; i++) {
    simp[i] = fanum[i] ^ simp[i];
}

for (int i = 0; i < 7; i++) {
    vibe[i] = fanum[i] ^ vibe[i];
}

for (int i = 0; i < 8; i++) {
    drip[i] = vibe[i % 7] ^ drip[i];
}

if (strcmp(simp, "r!zz13r") != 0) check_rule(10);
if (strcmp(vibe, "5ki8idi") != 0) check_rule(11);

char woke[9] = {0x40, 0x05, 0x5c, 0x48, 0x59, 0x0f, 0x5a, 0x5b, 0x00};
if (strcmp(drip, woke) != 0) check_rule(12);

if ((input[24] | input[19]) != '0') check_rule(13);
if ((input[24] | input[27]) != '0') check_rule(14);
if (input[26] != input[44]) check_rule(15);

char clout[7] = "      ";
strncpy(clout, input + 8, 6);
for (int i = 0; i < 6; i++) {
    clout[i] = clout[i] + 1;
}

char zest[7] = {0x62, 0x6e, 0x60, 0x75, 0x69, 0x34, 0x00};
if (strcmp(clout, zest) != 0) check_rule(16);

char snack[6] = "     ";
char L[6] = {0x05, 0x17, 0x01, 0x01, 0x1d, 0x00};
strncpy(snack, input + 37, 5);
for (int i = 0; i < 5; i++) {
    snack[i] = snack[i] ^ zest[i];
}

if (strcmp(snack, L) != 0) check_rule(17);

printf("All rules vibe! ๐Ÿ˜๐Ÿ‘‰๐Ÿ‘ˆ Flag is correct! โœ…\n");
}

Solve Script

1
2
3
4
def check_constraints(flag):
    # Rule 0: Length must be 51
    if len(flag) != 51:
        return False, 0

Rule 1: Must start with โ€œudctfโ€

1
2
if flag[:5] != "udctf":
    return False, 1

Rule 2: Must end with โ€œ}โ€

1
2
if flag[-1] != '}':
    return False, 2

Rule 3: (flag[5]*4)%102 == โ€˜Tโ€™

1
2
if (ord(flag[5]) * 4) % 102 != ord('T'):
    return False, 3

Rule 4: (flag[35] | flag[33]) == 0x69

1
2
if (ord(flag[35]) | ord(flag[33])) != 0x69:
    return False, 4

Rule 5: flag[6] ^ flag[31] must be 0

1
2
if ord(flag[6]) ^ ord(flag[31]):
    return False, 5

Rule 6: (flag[31] + flag[35]) == (flag[6] * 2)

1
2
if (ord(flag[31]) + ord(flag[35])) != (ord(flag[6]) * 2):
    return False, 6

Rule 7: These must all be equal

1
2
3
eq_count = (flag[7] == flag[10]) + (flag[14] == flag[23]) + (flag[28] == flag[36])
if eq_count != 3:
    return False, 7

Rule 8: Chain of equalities

1
2
if not (flag[42] == flag[28] and flag[36] == flag[23] and flag[10] == flag[42]):
    return False, 8

Rule 9: flag[10] must be โ€˜_โ€™

1
2
if flag[10] != '_':
    return False, 9

Rules 10-12: XOR operations

1
fanum = [0x47, 0x4a, 0x13, 0x42, 0x58, 0x57, 0x1b]

Check simp (Rule 10)

1
2
3
4
5
simp = list(flag[29:36])
for i in range(7):
    simp[i] = chr(ord(simp[i]) ^ fanum[i])
if ''.join(simp) != "r!zz13r":
    return False, 10

Check vibe (Rule 11)

1
2
3
4
5
vibe = list(flag[43:50])
for i in range(7):
    vibe[i] = chr(ord(vibe[i]) ^ fanum[i])
if ''.join(vibe) != "5ki8idi":
    return False, 11

Check drip (Rule 12)

1
2
3
4
5
6
7
drip = list(flag[15:23])
woke = [0x40, 0x05, 0x5c, 0x48, 0x59, 0x0f, 0x5a, 0x5b]
decrypted_drip = []
for i in range(8):
    decrypted_drip.append(ord(flag[43 + (i % 7)]) ^ fanum[i % 7] ^ ord(drip[i]))
if decrypted_drip != woke:
    return False, 12

Rules 13-14: Position 24 constraints

1
2
3
4
if (ord(flag[24]) | ord(flag[19])) != ord('0'):
    return False, 13
if (ord(flag[24]) | ord(flag[27])) != ord('0'):
    return False, 14

Rule 15: flag[26] == flag[44]

1
2
if flag[26] != flag[44]:
    return False, 15

Rule 16: clout/zest relationship

1
2
3
4
5
6
clout = list(flag[8:14])
for i in range(6):
    clout[i] = chr(ord(clout[i]) + 1)
zest = [0x62, 0x6e, 0x60, 0x75, 0x69, 0x34]
if [ord(c) for c in clout[:6]] != zest:
    return False, 16

Rule 17: snack/L relationship

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
L = [0x05, 0x17, 0x01, 0x01, 0x1d]
snack = list(flag[37:42])
for i in range(5):
    if ord(snack[i]) ^ zest[i] != L[i]:
        return False, 17

return True, -1

def test_flag(flag):
    result, rule = check_constraints(flag)
    if not result:
        print(f"Flag failed at rule {rule}")
    else:
        print("Flag passed all rules!")
    return result

Test the flag

1
2
3
4
flag = "udctf{Hi_am_th3_un5p0k3n_0_!0_5ki8idi_gyatt_r!zz13r}"
print(f"Testing flag: {flag}")
print(f"Flag length: {len(flag)}")
test_flag(flag)

udctf{Hi_am_th3_un5p0k3n_0_!0_5ki8idi_gyatt_r!zz13r}

Upon analyzing the flag, we noticed the 0_!0 segment, which was key to completing the flag. After decoding the logic and piecing together the partial flag, we conducted a quick Google search for common brainrot words and discovered that โ€œohioโ€ is a popular choice. This helped us fill in the missing 4-letter word.0_!0 u d c t f {Hi_am _th3_un5p0k3n_0_!0 _5ki8idi_gyatt_r!zz13r}

Final flag: udctf{i_am_th3_un5p0k3n_0h!0_5ki8idi_gyatt_r!zz13r}

AlgebrarbeglA - misc

Challenge

78! - k = k - !87

Solve for k flag format is udctf{k}

Solution

For this challenge, we used Wolfram Alpha to handle the large factorial values directly.

image

Flag udctf{387700288526444839185460979130991103610316350951544192244807199359099600806691328655309595021094080317314686982970896828895806969367}

Just a day at the breach - lambda - crypto - web

Question:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
import json
import zlib

def lambda_handler(event, context):
    try:
        payload=bytes.fromhex(event["queryStringParameters"]["payload"])
        flag = os.environ["flag"].encode()
        message = b"Your payload is: %b\nThe flag is: %b" % (payload, flag)
        compressed_length = len(zlib.compress(message,9))
    except ValueError as e:
        return {'statusCode': 500, "error": str(e)}

    return {
        'statusCode': 200,
        'body': json.dumps({"sniffed": compressed_length})
    }

Solution: char by char brute. Getting the smallest length is the correct character and appending it to the other resolved ones to complete the flag

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
import requests
import string
from concurrent.futures import ThreadPoolExecutor, as_completed

url = "https://55nlig2es7hyrhvzcxzboyp4xe0nzjrc.lambda-url.us-east-1.on.aws/"
known_flag = "udctf"
possible_chars = string.ascii_letters + string.digits + "{}_"

def get_compressed_length(char):
    payload = known_flag + char
    
    hex_payload = payload.encode().hex()
    response = requests.get(url, params={'payload': hex_payload})
    return char, response.json().get('sniffed')

while True:
    min_length = float('inf')
    next_char = ''

    with ThreadPoolExecutor(max_workers=10) as executor:  # Adjust max_workers based on network capacity
        futures = [executor.submit(get_compressed_length, char) for char in possible_chars]

        for future in as_completed(futures):
            char, compressed_length = future.result()
            if compressed_length < min_length:
                min_length = compressed_length
                next_char = char

    if next_char:
        known_flag += next_char
        print(f"Current flag: {known_flag}")
    else:
        break
        ``````

Flag:`udctf{huffm4n_br34ched_l3t5_go` 
![Pasted image 20241109041034](https://gist.github.com/user-attachments/assets/8831a5a1-b92b-4bf7-9740-75ecdc06dc8b)


# Nonogram Pt. 1: Simple Enough - crypto

### Question:
When you get past the puzzle, you now face a classic encryption / old-school stego encoding. Wrap the text you find in `UDCTF{TEXTHERE}``.

### Solution

This was a nonogram puzzle

- Nonogram Solver: We started with the nonogram puzzle and used https://fedimser.github.io/nonogram tool to recreate and solve the puzzle pattern provided. The solution revealed a coded message that required further analysis.

- Cipher Identification: Next, we used dcode.frโ€™s Cipher Identifier to determine which cipher could have been used. We tested multiple options, and Bacon Cipher emerged as the solution.

- Decoding: With Bacon Cipher identified, we decoded the message, which revealed the encoded string

![Pasted image 20241109215436](https://gist.github.com/user-attachments/assets/34b7e258-2457-460c-b044-ea4a72e3cd5b)


![image](https://gist.github.com/user-attachments/assets/99186959-87aa-4b0f-af1e-de383b93b23c)

 flag: UDCTF{PIXELATED}

# Training Problem: Intro to RSA - crypto

### Question:

```python
In [9]: p = getPrime(128)
In [10]: q = getPrime(128)
In [11]: N = p*q
In [12]: bytes_to_long(flag) < N
Out[12]: True
In [13]: print(pow(bytes_to_long(flag), 65537, N), N)
9015202564552492364962954854291908723653545972440223723318311631007329746475 51328431690246050000196200646927542588629192646276628974445855970986472407007

Solution

Step 1: Understand the RSA Parameters Given

In RSA encryption, the public key consists of:

N = p x ๐‘ž N=pร—q: the product of two prime numbers. ๐‘’: the public exponent, which is given as 65537.

We also know: flag is a message that was encrypted with the public key, and The result of the encryption (c = pow(bytes_to_long(flag), e, N)) is given.

The goal is to find the original message, flag.

Step 2: Factorize N

We use FactorDb Query to factorize ๐‘ into ๐‘ and ๐‘ž

Step 3: Calculate the Private Key Exponent

The private exponent ๐‘‘ is calculated using the modular inverse of e e with respect to (pโˆ’1)(๐‘žโˆ’1) which allows us to decrypt messages encrypted with ๐‘’ In mathematical terms: mod((pโˆ’1)(qโˆ’1))

Step 4: Decrypt the Ciphertext

Now that we have ๐‘‘ we can decrypt the ciphertext ๐‘ using the RSA decryption formula:

Here, m is the decrypted message as an integer. We can then convert it back to bytes and decode it to reveal the original flag.

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 Crypto.Util.number import long_to_bytes

# Extended GCD for modular inverse
def extended_gcd(a, b):
    if a == 0:
        return b, 0, 1
    gcd, x1, y1 = extended_gcd(b % a, a)
    x = y1 - (b // a) * x1
    y = x1
    return gcd, x, y

# Modular inverse using extended GCD
def modinv(a, m):
    _, x, _ = extended_gcd(a, m)
    return (x % m + m) % m

# Given values
N = 51328431690246050000196200646927542588629192646276628974445855970986472407007
e = 65537
c = 9015202564552492364962954854291908723653545972440223723318311631007329746475

# Factored values from factordb
p = 186574907923363749257839451561965615541
q = 275108975057510790219027682719040831427

# Verify N = p * q
assert p * q == N, "Factors are correct!"

# Calculate ฯ†(N)
phi = (p - 1) * (q - 1)

# Calculate private key d
d = modinv(e, phi)

# Decrypt the message
m = pow(c, d, N)

# Convert to bytes and print the flag
flag = long_to_bytes(m)
print(flag)

udctf{just_4_s1mpl3_RS4}

image

Bees in Space

Get the binary from the txt provided

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
def analyze_spaces(text):
    # Split into lines
    lines = text.split('\n')
    
    # Analyze each line
    for i, line in enumerate(lines):
        # Get spaces/tabs at start of line
        leading_spaces = len(line) - len(line.lstrip())
        # Get count of spaces between words
        word_spaces = len([s for s in line.split() if s.isspace()])
        # Convert spaces/tabs to binary (space=0, tab=1)
        binary = ''.join(['1' if c == '\t' else '0' if c.isspace() else '' for c in line])
        
        print(f"Line {i+1}:")
        print(f"Leading spaces: {leading_spaces}")
        print(f"Spaces between words: {word_spaces}")
        print(f"Binary pattern: {binary}")
        print()

# Process the text
text = """According to all known	laws of	aviation, there	is no	way
a	bee
should be able to fly. Its	wings are too small	to get its
fat	little
body off the ground. The bee,	of course, flies anyway because	bees	don't
care	what
humans think is impossible. Yellow, black.	Yellow, black.	Yellow, black.	Yellow, black. Ooh,
black	and
yellow! Let's shake it up a	little. Barry! Breakfast is	ready!	Coming! Hang
on	a
second. Hello? Barry? Adam? Can you	believe	this	is	happening? I	can't.	I'll
pick	you
up. Looking sharp. Use the stairs,	Your	father	paid good	money	for	those.
Sorry.	I'm
excited. Here's the graduate. We're very	proud	of you,	son. A perfect report
card,	all
B's. Very proud. Ma! I got a	thing	going here. You got	lint
on	your
fuzz. Ow! That's me! Wave to	us!	We'll	be in	row 118,000. Bye!
Barry,	I
told you, stop flying in the house!	Hey,	Adam. Hey, Barry.	Is	that
fuzz	gel?
A little. Special day, graduation. Never	thought	I'd	make it. Three	days	grade
school,	three
days high school. Those were awkward.	Three	days	college. I'm glad I took
a	day
and hitchhiked around The Hive. You did	come	back different.	Hi, Barry. Artie,
growing	a
mustache? Looks good. Hear about Frankie?	Yeah.	You going to the	funeral?	No,
I'm	not
going. Everybody knows, sting someone, you die.	Don't	waste it on	a	squirrel.
Such	a
hothead. I guess he could have	just gotten	out	of	the	way.	I
love	this
incorporating an amusement park into our day.	That's	why we don't need	vacations.
Boy,	quite
a bit of pomp under the circumstances.	Well,	Adam, today	we are	men.
We	are!
Bee-men. Amen! Hallelujah! Students, faculty, distinguished	bees, please	welcome	Dean	Buzzwell.	Welcome,	New
Hive	City
graduating class of 9:15. That concludes	our	ceremonies And begins your	career	at
Honex	Industries!
Will we pick our job today? I	heard	it's just orientation. Heads up!
Here	we
go. Keep your hands and antennas inside	the	tram at all times. Wonder
what	it'll
be like? A little scary. Welcome	to	Honex, a	division	of Honesco and
a	part
of the Hexagon Group. This is	it!	Wow.	Wow.	We	know that	you,
as	a
bee, have worked
your
whole
life"""  # Your full text here

analyze_spaces(text)

Pasted image 20241109183034 Pasted image 20241109183034

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
def decode_binary_pattern(lines):
    # Extract only the meaningful binary patterns (skip the single "1" lines)
    patterns = [line.split(':')[-1].strip() for line in lines if "Binary pattern" in line and line.split(':')[-1].strip() != '1']
    
    # Convert each binary pattern to decimal/ASCII
    decoded = ""
    for pattern in patterns:
        if pattern:  # Skip empty patterns
            try:
                # Convert binary to decimal
                decimal = int(pattern, 2)
                # Convert decimal to character
                char = chr(decimal)
                decoded += char
            except:
                continue
    
    return decoded


test_lines = [
    "Binary pattern: 0001010101",
    "Binary pattern: 1",
    "Binary pattern: 000001000100",
    "Binary pattern: 1",
    "Binary pattern: 000001000011",
    "Binary pattern: 1",
    "Binary pattern: 000001010100",
    "Binary pattern: 1",
    "Binary pattern: 000001000110",
    "Binary pattern: 1",
    "Binary pattern: 000001111011",
    "Binary pattern: 1",
    "Binary pattern: 000001110111",
    "Binary pattern: 1",
    "Binary pattern: 000001101000",
    "Binary pattern: 1",
    "Binary pattern: 000000110001",
    "Binary pattern: 1",
    "Binary pattern: 000001110100",
    "Binary pattern: 1",
    "Binary pattern: 000000110011",
    "Binary pattern: 1",
    "Binary pattern: 000001110011",
    "Binary pattern: 1",
    "Binary pattern: 000001110000",
    "Binary pattern: 1",
    "Binary pattern: 000000110100",
    "Binary pattern: 1",
    "Binary pattern: 000001100011",
    "Binary pattern: 1",
    "Binary pattern: 000000110011",
    "Binary pattern: 1",
    "Binary pattern: 000001011111",
    "Binary pattern: 1",
    "Binary pattern: 000000110001",
    "Binary pattern: 1",
    "Binary pattern: 000000110101",
    "Binary pattern: 1",
    "Binary pattern: 000001011111",
    "Binary pattern: 1",
    "Binary pattern: 000001100011",
    "Binary pattern: 1",
    "Binary pattern: 000000110000",
    "Binary pattern: 1",
    "Binary pattern: 000000110000",
    "Binary pattern: 1",
    "Binary pattern: 000001101100",
    "Binary pattern: 1",
    "Binary pattern: 000001111101",
    "Binary pattern: 1",
    "Binary pattern: 00",
    "Binary pattern: ",
    "Binary pattern: "
]

result = decode_binary_pattern(test_lines)
print("Decoded message:", result)

Pasted image 20241109181018

This post is licensed under CC BY 4.0 by the author.