XS2: Looper - xor school
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}
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}
๐
ฑ๏ธ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.
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`

# 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


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