UMDCTF 2025

比赛地址:UMDCTF 2025

比赛时间:26 Apr 2025 06:00 CST - 28 Apr 2025 06:00 CST

Misc

find the seeds

Challenge

can u help Alice find her seeds in the bin? She’s pretty sure the bin hasn’t been dumped since it was generated.

1
2
3
4
5
6
7
8
9
10
11
12
import random
import time

seed = int(time.time())
random.seed(seed)

plaintext = b"UMDCTF{REDACTED}"
keystream = bytes([random.getrandbits(8) for _ in range(len(plaintext))])
ciphertext = bytes([p ^ k for p, k in zip(plaintext, keystream)])

with open("secret.bin", "wb") as f:
f.write(ciphertext)

Solution

加密过程使用了 XOR 操作,将 plaintext 和一个由随机数生成器(random.getrandbits(8))生成的 keystream 进行逐字节异或操作,得到 ciphertext

已知 XOR 的性质:如果 A ^ B = C,那么 C ^ B = A。因此,如果我们知道 ciphertextkeystream,可以通过 ciphertext ^ keystream 还原出 plaintext

因此可以爆破加密时的时间戳,使用该时间戳重新生成 keystream,通过 ciphertext ^ keystream 还原出 plaintext

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
import random
import time

# 读取密文文件
with open("secret.bin", "rb") as f:
ciphertext = f.read()

# 已知明文的固定前缀
KNOWN_PREFIX = b"UMDCTF{"

# 尝试还原 plaintext
def recover_plaintext(ciphertext):
# 获取当前时间戳
current_time = int(time.time())

# 时间设置长一点进行爆破
for seed in range(current_time - 360000, current_time + 1):
random.seed(seed)

# 重新生成 keystream
keystream = bytes([random.getrandbits(8) for _ in range(len(ciphertext))])

# 还原 plaintext
plaintext = bytes([c ^ k for c, k in zip(ciphertext, keystream)])

# 检查 plaintext 是否以已知前缀开头
if plaintext.startswith(KNOWN_PREFIX):
print(f"Seed found: {seed}")
return plaintext

return None

# 还原 plaintext
plaintext = recover_plaintext(ciphertext)
print(plaintext.decode('utf-8'))
1
2
Seed found: 1745447710
UMDCTF{pseudo_entropy_hidden_seed}
1
UMDCTF{pseudo_entropy_hidden_seed}

tiktok-ban

Challenge

Oh snap! Oh crap! TikTok is banned in Ohio!

nc challs.umdctf.io 32300

Solution

服务端运行了一个 dnsmasq 实例,配置了一个 值为 flag 的 TXT 记录 tiktok.com

服务端首先读取 4 字节的长度信息(大端格式),然后根据该长度读取后续的数据。

如果数据中包含 tiktok\x03com(即 DNS 查询格式中的 tiktok.com),服务器会返回一段固定的错误消息,否则就返回 flag。

因此我们需要构造一个 DNS 请求,绕过检查查询 tiktok.com 的 TXT 记录,从而获取 flag

注意到服务端使用 if b'tiktok\x03com' in req 进行检查,域名是不区分大小写的,然而这里的判断是区分大小写的,因此我们可以通过利用大小写绕过构造一个等价的域名。

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
from pwn import *
import struct

HOST = "challs.umdctf.io"
PORT = 32300

# 构造 DNS 查询请求
def create_dns_query():
# DNS Header (固定字段)
transaction_id = b"\x12\x34" # 事务 ID,可以随意设置
flags = b"\x01\x00" # 标志位:标准查询
questions = b"\x00\x01" # 问题数:1
answer_rrs = b"\x00\x00" # 回答资源记录数:0
authority_rrs = b"\x00\x00" # 权威资源记录数:0
additional_rrs = b"\x00\x00" # 附加资源记录数:0
header = transaction_id + flags + questions + answer_rrs + authority_rrs + additional_rrs

# DNS Question (查询部分)
qname = b"\x06Tiktok\x03com\x00" # 域名:Tiktok.com(大小写绕过)
qtype = b"\x00\x10" # 查询类型:TXT (16)
qclass = b"\x00\x01" # 查询类:IN (1)
question = qname + qtype + qclass

# 组合完整的 DNS 请求
dns_query = header + question
return dns_query

def main():
conn = remote(HOST, PORT)
dns_query = create_dns_query()

# 发送长度信息和 DNS 请求
conn.send(struct.pack(">I", len(dns_query))) # 大端格式的长度
conn.send(dns_query)

response = conn.recvall()
print(response.decode())
conn.close()

if __name__ == "__main__":
main()

OSINT

swag-like-ohio

Challenge

swag like ohio. down in ohio. swag like ohio. down in ohio. anyway we seem to be on a bridge. what’s the address of the bridge?

flag will look like: UMDCTF

UMDCTF2025-1

Solution

UMDCTF2025-2

先识图找 ohio 找到这篇文章 Vacancies — Marietta Main Street

再用这篇文章里的图接着搜

UMDCTF2025-3

再找到维基百科上的这张图 File:Ohio - Marietta - Dime Bank.jpg - Wikimedia Commons,得知这个建筑是 Marietta Dime Bank

UMDCTF2025-4

在这里 200 Putnam Street in Historic Downtown Marietta, OH 找到地址 200 Putnam Street, Marietta, OH 45750

UMDCTF2025-5

到谷歌地图直接搜

UMDCTF2025-6

Putnam Bridge - Google 地圖

1
UMDCTF{Putnam Bridge, Marietta, OH 45750}

sunshine

Challenge

what a nice sunny day. what is the full address of house number 356?

flag will look like: UMDCTF

UMDCTF2025-7

Solution

UMDCTF2025-8

搜索题目给定的这栋房子发现它是篮球篮球运动员 LeBron James 童年的住所

在第一个搜索结果 Where LeBron James Lives: A Peek Into LeBron’s Homes Interbasket 中发现 LeBron’s Childhood Home in Akron, Ohio

进一步搜索

UMDCTF2025-9

在第一个搜索结果 LeBron James’ childhood home in Akron, OH (Google Maps) 中找到题目给定的地址(甚至街景都一模一样)

356 Hillwood Dr - Google 地圖

UMDCTF2025-10

位于 356 Hillwood Dr, Akron, OH 44320美國

1
UMDCTF{356 Hillwood Dr, Akron, OH 44320}

beauty

Challenge

truly a beautiful panorama. ohio is not always ugly. i really wanna know who made this pano tho. what’s their name?

flag will look like: UMDCTF

UMDCTF2025-11

Solution

UMDCTF2025-12

搜到这篇百科 List of tallest buildings in Ohio - Wikipedia

UMDCTF2025-13

找到这个建筑 AEP Building - Wikipedia,然后到谷歌地图上搜

UMDCTF2025-14

Battelle Riverfront Park - Google 地圖

1
UMDCTF{Neil Larimore}

the-master

Challenge

trust me bro, i know what im talking about. im the master when it comes to these things. what street are we on?

flag will look like: UMDCTF{Campus Dr, College Park, MD 20742}

UMDCTF2025-15

Solution

UMDCTF2025-16

识图找到维基百科上的这张图片 File:Lore City UMC.jpg - Wikimedia Commons

下面给出了拍摄这张图时所在的坐标 GeoHack - File:Lore City UMC.jpg

打开谷歌地图 [39°59’02.0"N 81°27’32.0"W - Google Maps](39°59’02.0"N 81°27’32.0"W - Google Maps)

最终可以定位到这里 190 Main St - Google Maps

1
UMDCTF{Main St, Lore City, OH 43755}

Nyt

the-mini

Challenge

Joel Fagliano has nothing on me. (flag is all caps)

Solution

UMDCTF2025-18

这是个填字游戏

UMDCTF2025-19

并且它的 solution 被锁住了

然而这里的 key 只有 4 位数,因此直接爆破

在 GitHub 上找了个处理 .puz 的 Python 库 alexdej/puzpy,然后就是写脚本调用接口爆破了

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
import puz

def brute_force_unlock(puzzle, max_key=10000):
"""
尝试暴力破解锁定的谜题。

:param puzzle: puz.Puzzle 对象
:param max_key: 最大密钥值,默认为 10000
:return: 解锁后的谜题对象(如果成功),否则返回 None
"""
for key in range(max_key):
if puzzle.unlock_solution(key):
print(f"成功解锁!密钥为: {key}")
return puzzle
print("未能找到正确的密钥。")
return None

def main():
# 文件路径
file_path = "the_mini.puz"

try:
# 读取 .puz 文件
puzzle = puz.read(file_path)

# 检查是否被锁定
if puzzle.is_solution_locked():
print("谜题被锁定,尝试暴力破解...")
unlocked_puzzle = brute_force_unlock(puzzle)

if unlocked_puzzle:
# 输出解锁后的解决方案
print("解锁后的解决方案:")
print(unlocked_puzzle.solution)
else:
print("暴力破解失败。")
else:
print("谜题未被锁定,直接输出解决方案:")
print(puzzle.solution)

except puz.PuzzleFormatError as e:
print(f"读取谜题文件时发生错误: {e}")
except Exception as e:
print(f"发生未知错误: {e}")

if __name__ == "__main__":
main()

运行得到输出

1
2
3
4
谜题被锁定,尝试暴力破解...
成功解锁!密钥为: 5727
解锁后的解决方案:
UMDCTFCANYOUBEATMYTIME...
1
UMDCTF{CANYOUBEATMYTIME}

Reverse

deobfuscation

Challenge

the chall is not that complex. the key is to read ASSEMBLY!

Solution

UMDCTF2025-17

入口函数的分析如图,只要提取固定数组 byte_402000byte_402034,然后通过异或运算就可以还原出正确的输入,下面是提取出的 byte_402000byte_402034 的内容

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
.data:0000000000402000 20                            byte_402000 db 20h                      ; DATA XREF: LOAD:00000000004000C0↑o
.data:0000000000402000 ; start:loc_40105D↑r
.data:0000000000402001 22 db 22h ; "
.data:0000000000402002 20 db 20h
.data:0000000000402003 26 db 26h ; &
.data:0000000000402004 35 db 35h ; 5
.data:0000000000402005 37 db 37h ; 7
.data:0000000000402006 14 db 14h
.data:0000000000402007 07 db 7
.data:0000000000402008 46 db 46h ; F
.data:0000000000402009 00 db 0
.data:000000000040200A 5A db 5Ah ; Z
.data:000000000040200B 17 db 17h
.data:000000000040200C 44 db 44h ; D
.data:000000000040200D 35 db 35h ; 5
.data:000000000040200E 52 db 52h ; R
.data:000000000040200F 0C db 0Ch
.data:0000000000402010 70 db 70h ; p
.data:0000000000402011 28 db 28h ; (
.data:0000000000402012 37 db 37h ; 7
.data:0000000000402013 1C db 1Ch
.data:0000000000402014 5B db 5Bh ; [
.data:0000000000402015 1D db 1Dh
.data:0000000000402016 70 db 70h ; p
.data:0000000000402017 16 db 16h
.data:0000000000402018 76 db 76h ; v
.data:0000000000402019 50 db 50h ; P
.data:000000000040201A 69 db 69h ; i
.data:000000000040201B 5C db 5Ch ; \
.data:000000000040201C 6E db 6Eh ; n
.data:000000000040201D 6C db 6Ch ; l
.data:000000000040201E 1B db 1Bh
.data:000000000040201F 12 db 12h
.data:0000000000402020 54 db 54h ; T
.data:0000000000402021 69 db 69h ; i
.data:0000000000402022 2D db 2Dh ; -
.data:0000000000402023 38 db 38h ; 8
.data:0000000000402024 06 db 6
.data:0000000000402025 23 db 23h ; #
.data:0000000000402026 11 db 11h
.data:0000000000402027 3D db 3Dh ; =
.data:0000000000402028 2F db 2Fh ; /
.data:0000000000402029 00 db 0
.data:000000000040202A 02 db 2
.data:000000000040202B 4A db 4Ah ; J
.data:000000000040202C 68 db 68h ; h
.data:000000000040202D 45 db 45h ; E
.data:000000000040202E 3B db 3Bh ; ;
.data:000000000040202F 64 db 64h ; d
.data:0000000000402030 1A db 1Ah
.data:0000000000402031 20 db 20h
.data:0000000000402032 55 db 55h ; U
.data:0000000000402033 05 db 5
.data:0000000000402034 ; char byte_402034[52]
.data:0000000000402034 75 byte_402034 db 75h ; DATA XREF: start+43↑r
.data:0000000000402035 6F db 6Fh ; o
.data:0000000000402036 64 db 64h ; d
.data:0000000000402037 65 db 65h ; e
.data:0000000000402038 61 db 61h ; a
.data:0000000000402039 71 db 71h ; q
.data:000000000040203A 6F db 6Fh ; o
.data:000000000040203B 75 db 75h ; u
.data:000000000040203C 75 db 75h ; u
.data:000000000040203D 76 db 76h ; v
.data:000000000040203E 69 db 69h ; i
.data:000000000040203F 45 db 45h ; E
.data:0000000000402040 60 db 60h ; `
.data:0000000000402041 70 db 70h ; p
.data:0000000000402042 7F db 7Fh ; 
.data:0000000000402043 65 db 65h ; e
.data:0000000000402044 54 db 54h ; T
.data:0000000000402045 77 db 77h ; w
.data:0000000000402046 63 db 63h ; c
.data:0000000000402047 74 db 74h ; t
.data:0000000000402048 68 db 68h ; h
.data:0000000000402049 42 db 42h ; B
.data:000000000040204A 53 db 53h ; S
.data:000000000040204B 54 db 54h ; T
.data:000000000040204C 45 db 45h ; E
.data:000000000040204D 03 db 3
.data:000000000040204E 3D db 3Dh ; =
.data:000000000040204F 7F db 7Fh ; 
.data:0000000000402050 31 db 31h ; 1
.data:0000000000402051 58 db 58h ; X
.data:0000000000402052 75 db 75h ; u
.data:0000000000402053 46 db 46h ; F
.data:0000000000402054 75 db 75h ; u
.data:0000000000402055 44 db 44h ; D
.data:0000000000402056 60 db 60h ; `
.data:0000000000402057 78 db 78h ; x
.data:0000000000402058 6A db 6Ah ; j
.data:0000000000402059 74 db 74h ; t
.data:000000000040205A 51 db 51h ; Q
.data:000000000040205B 4F db 4Fh ; O
.data:000000000040205C 1C db 1Ch
.data:000000000040205D 5F db 5Fh ; _
.data:000000000040205E 76 db 76h ; v
.data:000000000040205F 79 db 79h ; y
.data:0000000000402060 0B db 0Bh
.data:0000000000402061 2D db 2Dh ; -
.data:0000000000402062 75 db 75h ; u
.data:0000000000402063 45 db 45h ; E
.data:0000000000402064 4B db 4Bh ; K
.data:0000000000402065 55 db 55h ; U
.data:0000000000402066 66 db 66h ; f
.data:0000000000402067 78 db 78h ; x

编写脚本还原

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
byte_402000 = [
0x20, 0x22, 0x20, 0x26, 0x35, 0x37, 0x14, 0x07,
0x46, 0x00, 0x5A, 0x17, 0x44, 0x35, 0x52, 0x0C,
0x70, 0x28, 0x37, 0x1C, 0x5B, 0x1D, 0x70, 0x16,
0x76, 0x50, 0x69, 0x5C, 0x6E, 0x6C, 0x1B, 0x12,
0x54, 0x69, 0x2D, 0x38, 0x06, 0x23, 0x11, 0x3D,
0x2F, 0x00, 0x02, 0x4A, 0x68, 0x45, 0x3B, 0x64,
0x1A, 0x20, 0x55, 0x05
]

byte_402034 = [
0x75, 0x6F, 0x64, 0x65, 0x61, 0x71, 0x6F, 0x75,
0x75, 0x76, 0x69, 0x45, 0x60, 0x70, 0x7F, 0x65,
0x54, 0x77, 0x63, 0x74, 0x68, 0x42, 0x53, 0x54,
0x45, 0x03, 0x3D, 0x7F, 0x31, 0x58, 0x75, 0x46,
0x75, 0x44, 0x60, 0x78, 0x6A, 0x74, 0x51, 0x4F,
0x1C, 0x5F, 0x76, 0x79, 0x0B, 0x2D, 0x75, 0x45,
0x4B, 0x55, 0x66, 0x78
]

correct_input = []
for i in range(len(byte_402000)):
correct_input.append(byte_402000[i] ^ byte_402034[i])

print("".join(chr(c) for c in correct_input))
1
UMDCTF{r3v3R$E-i$_Th3_#B3ST#_4nT!-M@lW@r3_t3chN!Qu3}