文章总结: 本文记录了2026年山东省某市网络安全职业技能大赛CTF解题思路,涵盖逆向、密码与杂项。逆向题通过去除花指令并逆向MSVC伪随机算法求解;密码题利用已知前缀对重复密钥异或进行推导恢复明文;杂项题深入剖析WPA2-PSK四次握手中PMK、PTK与MIC的生成原理,并附带一套基于Scapy的通用WIFI握手爆破Python脚本,支持多版本加密,对流量分析实战具有较高参考价值。 综合评分: 82 文章分类: CTF,逆向分析,安全工具,实战经验
2026年山东省某市网络安全职业技能大赛(含题目、WP)
原创
蓬莱人形 蓬莱人形
AI丽丝的人偶工坊
2026年8月6日 12:34 山东
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
好久没打过CTF了,感觉已经完全废废了,记录一下学习学习~有不足或者错误的地方,还请劳烦指出(鞠躬)
(省流看文章末尾蓝奏链接,含附件和WP)
RE
题目叫8E,这里有一个8E的指令,上面有个jz和jnz的花指令跳过,E8是垃圾指令,忽略即可
把中间的花指令nop掉,下面的代码直接P恢复即可
使用F5查看
一个伪随机,种子是0xDEADBEEF,编译环境是MSVC,所以找对应的伪随机就行
import ctypes # MSVC rand() implementationclass MSVCRand: def __init__(self, seed): self.state = seed def rand(self): self.state = (self.state * 214013 + 2531011) & 0xFFFFFFFF return (self.state >> 16) & 0x7FFF target = [0x51, 0x7D, 0xE1, 0x4C, 0x43, 0x8A, 0xDE, 0xDC, 0x48, 0x8F, 0x07, 0xE2, 0x1F, 0xC0, 0x42, 0x0D, 0xAE, 0xEE, 0x79, 0x99, 0xDA, 0x53, 0x74, 0x5A, 0xF7, 0x5F, 0x14, 0xF9, 0xBC, 0x01, 0xDA, 0x77, 0x9F, 0xD5, 0xE3, 0x4E, 0x65, 0x02, 0xE9, 0x2A] rng = MSVCRand(0xDEADBEEF)result = list(target) for i in range(0, 40, 2): v3 = rng.rand() r = v3 % 255 a = result[i] b = result[i + 1] # Reverse: a ^= b a = a ^ b # Reverse: b -= a ^ r => b += a ^ r b = (b + (a ^ r)) & 0xFF # Reverse: a += r => a -= r a = (a - r) & 0xFF result[i] = a result[i + 1] = b flag = ''.join(chr(c) for c in result)print(flag)
DASCTF{ae457007d86e4509587fc27fa32e3ce3}
Crypt
重复密钥异或
from math import gcd hex_output = '2899d080c62a8379947adb554e92c83f1df96b54e1e6f4a20d9b7994768c544b96c03d1cfb695aa5c782c12fac5ad82ada594a98ca3c15fb355ebebbfaf75bc87dc02ada551d99cf381df9345cbcb5bed62dab5ff70996094b94ce3617f83d5ce0b1a5aa559d2b932e8e094b98993712fc355ee1b3a7a411bc5df00cb92a07c4cf3a13f23f5fe8b3fba00ac025c678dd0d1fc4cf3644f33a5be0b1faa208ce61e70ebe2f28e7836a12fe3b55eab0f3a254ca7a9b76885b4cc09b6a12f26c54efb4fba055c8789532a92d2fe2ac495eae3a59eebaf1a15cc8249129d5551996c86e46ae3a55b9bbf4a554ca25932bdb1138e0ab4c718d7609efb6f5ab5ecb2c9377df0a44989d3815aa6e09efbaa2aa5bcf249176dd084adcbc4e7688592aa3e6f4a75ac12e907fdd544ec7c03640fc3d0dbbe6f4ab0dc02b9477df554cc5ce72618a5e2f8cc5b8f75bcd2a9a7dde5c4c99ca691df2685be8e2a0f75bc17d9b78da544e98c86b13b6492d8bc097d4179d2b9679d45e4f91c83717ad3555bdb4f3f30f9d2b9a2ed55b4b99ca3615af3b119cc290d138be67c678d85a4593cb3f15f33f0ae0baa6a55c997fc678d40d4496cf3717f23d08eefe87d33fbb48e534885b4997c13d16fb3d54eae5fbab09cf2cc22c885b45c0c03812f33f55e8e7f5ef28b94fe01bab171996cd391cf93e5ce8bbf1f454c179947f8c0f1996c16e1dfc3a54eabaf3f65a8558e21cae383ada9d3810fd345eebb3f3aa5e9e249a2ada5c1dc29d381caa355befbbf1ab5c9c2ade0bac3f3ff5be7440fc385ae1b1f0a25cc02ec577d4094b91996c40fc340de0b4f4aa5ec12cc77990283df2bb5b63b0685bedb5faa05fc82c9b7d8b5445c4cf3f44a8685be1e2fba55bc02e9a7f895a01e5b95c669f4b17bdb4f6a455ca2f937fd55e1a99c16a12fb6c0fbdb4faf354cf2b9b7dd45c1897854b64984e389ef8a6a559ce25917cdd5c44939e371cae3a5cb9e0a6a55599249478d55e45919c39588f4c3f9bd785e909cf299576df5f4c91c03d43f33409efb3a2f109cf25c277da5b4493c13f41fd70'O = bytes.fromhex(hex_output) for L, K in [(40, 19), (19, 40)]: print(f'\\nTrying L={L} (flag_len), K={K} (key_len)...') R = [None] * K F = [None] * L # Known prefix prefix = 'DASCTF{' for i, ch in enumerate(prefix): F[i] = ord(ch) # Recover R from known F for t in range(len(O)): i = t % L if F[i] is not None: j = t % K val = O[t] ^ F[i] if R[j] is None: R[j] = val elif R[j] != val: print(f' Inconsistency at R[{j}]: {R[j]} vs {val}') break else: if None in R: print(f' Missing R bytes: {[j for j,v in enumerate(R) if v is None]}') else: # Recover F from R for t in range(len(O)): i = t % L if F[i] is None: j = t % K F[i] = O[t] ^ R[j] flag = ''.join(chr(c) if 32 <= c < 127 else '?' for c in F) print(f' Flag: {flag}') print(f' Hex: {bytes(F).hex()}')
Misc
题目:小明忘记了自己的WIFI密码,但是他知道密码很简单。
俩流量包,一个哥斯拉?一个WIFI流量,直接看WIFI流量就行
直接搜索eapol
对应M1和M2,提取关键数据
另外,又找了一个更直观的讲key_info的表补充讲解:
802.11这块我自己原来也没学明白,所以补习了一下
一般来说,WIFI连接( WPA2-PSK 算法)分四次握手,简称M1/M2/M3/M4,过程中涉及到的设备有AP(路由器)和STA(连接设备)
这里就以 WPA2-PSK 举个例子,讲一下WIFI的正常算法:
用户密码password->(PBKDF2+SSID+4096次迭代)->PMK(32字节主密钥,长期不变,存在AP和STA里,注意,PMK不是直接加密数据用的,可以理解为一个ID一样的东西)->每次连接协商一个临时的PTK->利用PTK的不同部分进行不同操作(包括完整性校验、密钥下发、加密单播数据等)
这里就需要记住,密码(对应唯一PMK)、加密“ID”(对应唯一PTK)、KCK校验值(PTK前16位)这三个东西就行,大致流程如下:
细节就不在这里写了,太多了,大概意思就是,我要知道密码,我就得要PMK,PMK可以用PTK反推,KCK是PTK的前16位,MIC又是KCK算出来的。经历这么一长串,也就是说,在ANonce和SNonce已知的情况下,MIC可以认作密码通过一种诡异的“签名算法”算出来的唯一值,当我们使用正确密码算这个的时候,是能够算出来唯一值与之对应的。这也就是为什么这道题能够直接爆破
有了上面的依据,直接开爆破就行,爆出来之后可以用wireshark解密试一下(我流量分析不咋地,Misc的这种脚本让我自己写是真写不出来。,还得是AI)
”””WPA/WPA2-PSK 四次握手爆破脚本(通用版)用法: python wp4crack.py -c Wifi.cap -w wordlist.txt [-s SSID]依赖: pip install scapy pip install pycryptodome # 仅当遇到 Version=3 的 AES-CMIC 时才需要”””import hashlib, hmac, sys, os, time, argparsefrom scapy.all import * def safe_print(s): sys.stdout.buffer.write((str(s) + ”\n”).encode(”utf-8”, errors=”replace”)) # ---------- 解析单个 EAPOL-Key 帧 ----------def parse_eapol_key(pkt): raw = bytes(pkt[EAPOL]) key_info = int.from_bytes(raw[5:7], ”big”) replay = int.from_bytes(raw[9:17], ”big”) # 优先用 scapy 的 EAPOL_KEY 层(内部已按规范处理好偏移),失败再 fallback 到手算 try: ek = pkt[EAPOL_KEY] key_nonce = bytes(ek.key_nonce) key_mic = bytes(ek.key_mic) except Exception: key_nonce = raw[17:49] key_mic = raw[81:97] return { ”raw”: raw, ”key_info”: key_info, ”key_ack”: (key_info >> 7) & 1, ”key_mic_bit”: (key_info >> 8) & 1, ”install”: (key_info >> 6) & 1, ”version”: key_info & 0x7, ”replay”: replay, ”nonce”: key_nonce, ”mic”: key_mic, ”addr1”: pkt[Dot11].addr1, ”addr2”: pkt[Dot11].addr2, ”addr3”: pkt[Dot11].addr3, } # ---------- 按 replay counter 把 M1/M2 配对 ----------def find_handshake_pairs(pkts): frames = [parse_eapol_key(p) for p in pkts if EAPOL in p] m1_by_replay = {} # replay -> M1 frame pairs = [] for f in frames: if f[”key_ack”] == 1 and f[”key_mic_bit”] == 0: # 这是 M1(或 M3 如果 install=1 且 mic=1,否则 M1) m1_by_replay.setdefault(f[”replay”], f) elif f[”key_ack”] == 0 and f[”key_mic_bit”] == 1: # M2 或 M4(STA 发的带 MIC 帧) m1 = m1_by_replay.get(f[”replay”]) if m1: pairs.append((”M1+M2”, m1, f)) elif f[”key_ack”] == 1 and f[”key_mic_bit”] == 1: # M3(AP 发的带 MIC 帧)— 也可以爆破,把它当作”M1”,配对自身算 # 这里把它也存进候选,供后续单独爆破用 pass # M3+M4 的爆破稍麻烦(M4 通常不含需验证的数据,但 M3 本身可以单独验证) return pairs, frames # ---------- 自动从 Beacon/ProbeResp 找 BSSID 对应的 SSID ----------def find_ssid(pkts, bssid): for p in pkts: if (Dot11Beacon in p or Dot11ProbeResp in p) and p[Dot11].addr3 == bssid: elt = p[Dot11Elt] while elt and isinstance(elt, Dot11Elt): if elt.ID == 0: return elt.info.decode(”ascii”, errors=”replace”) elt = elt.payload return None # ---------- 密钥派生 ----------def compute_ptk(pmk, ap_mac, sta_mac, anonce, snonce): A = b”Pairwise key expansion” B = (min(ap_mac, sta_mac) + max(ap_mac, sta_mac) + min(anonce, snonce) + max(anonce, snonce)) ptk = b”” for i in range(4): ptk += hmac.new(pmk, A + b”\x00” + B + bytes([i]), hashlib.sha1).digest() return ptk[:64] def compute_mic(kck, eapol_zeroed, version): if version == 1: return hmac.new(kck, eapol_zeroed, hashlib.md5).digest() elif version == 2: return hmac.new(kck, eapol_zeroed, hashlib.sha1).digest()[:16] elif version == 3: # AES-128-CMAC,标准库没,用 pycryptodome from Crypto.MAC import CMAC from Crypto.Cipher import AES c = CMAC.new(kck, ciphermod=AES) c.update(eapol_zeroed) return c.digest()[:16] else: raise RuntimeError(f”未知 key_descriptor_version={version}”) # ---------- 用 scapy 的 key_mic 字段长度定位偏移 ----------def find_mic_offset(eapol_bytes): # 标准 802.11i 终稿: offset 81;早期 WPA1 (KeyLength 1字节): offset 80 # 简单判断 EAPOL 帧的整体长度——配合 key_data_length 来反推更稳 # 这里就用标准 81;如果 MIC 全零且尝试反复不对,再试 80 return 81 def try_password(password, ssid, ap_mac, sta_mac, anonce, snonce, m_eapol, captured_mic, version, mic_offset=81): pmk = hashlib.pbkdf2_hmac(”sha1”, password.encode(), ssid.encode(), 4096, 32) ptk = compute_ptk(pmk, ap_mac, sta_mac, anonce, snonce) kck = ptk[:16] eapol_zeroed = bytearray(m_eapol) for i in range(16): eapol_zeroed[mic_offset + i] = 0 mic = compute_mic(kck, bytes(eapol_zeroed), version) return mic == captured_mic # ---------- 主流程 ----------def main(): ap = argparse.ArgumentParser() ap.add_argument(”-c”, ”--cap”, required=True, help=”抓包文件 .cap/.pcap”) ap.add_argument(”-w”, ”--wordlist”, required=True, help=”字典文件”) ap.add_argument(”-s”, ”--ssid”, help=”手动指定 SSID(缺省自动从 Beacon 提取)”) args = ap.parse_args() pkts = rdpcap(args.cap) safe_print(f”读 {args.cap}: {len(pkts)} 个包”) pairs, all_frames = find_handshake_pairs(pkts) safe_print(f”找到 {len(pairs)} 对 M1+M2”) if not pairs: safe_print(”没有可用 M1+M2 对,无法爆破”); return 1 _, m1, m2 = pairs[0] ap_mac = bytes(int(b, 16) for b in m1[”addr2”].split(”:”)) sta_mac = bytes(int(b, 16) for b in m2[”addr2”].split(”:”)) anonce, snonce = m1[”nonce”], m2[”nonce”] captured_mic, version = m2[”mic”], m2[”version”] m2_raw = m2[”raw”] ssid = args.ssid or find_ssid(pkts, m1[”addr2”]) or ”” if not ssid: safe_print(”无法自动提取 SSID,请用 -s 指定”); return 1 safe_print(f”SSID = {ssid}”) safe_print(f”AP = {m1['addr2']} STA = {m2['addr2']}”) safe_print(f”MIC = {captured_mic.hex()} Version = {version}”) with open(args.wordlist, ”r”, encoding=”utf-8”, errors=”replace”) as f: words = [w.strip() for w in f if w.strip()] safe_print(f”\n字典 {len(words)} 词,开始爆破”) t0 = time.time() mic_offset = find_mic_offset(m2_raw) for i, pwd in enumerate(words): ok = try_password(pwd, ssid, ap_mac, sta_mac, anonce, snonce, m2_raw, captured_mic, version, mic_offset) if ok: elapsed = time.time() - t0 safe_print(f”\n*** KEY FOUND: {pwd} ***”) safe_print(f”耗时 {elapsed:.2f}s”) return 0 if i and i % 500 == 0: rate = i / (time.time() - t0) safe_print(f” 进度 {i}/{len(words)} 速率 {rate:.1f} pwd/s”) safe_print(f”字典跑完未命中,耗时 {time.time()-t0:.2f}s”) return 1 if __name__ == ”__main__”: sys.exit(main())
密码是password(确实对上了题干:密码很简单),哥斯拉那个没看,不知道有啥用
碎碎念:
原来参加比赛是看谁准备的脚本多,谁脑子好使分析的快,有能力的抢枪一血,差一点的的做出来就行。
现在就看谁的本地模型好用,谁的harness能够发挥极限实力了。
AI发展的快,现在内部比武都开始库库用AI做题了,估计明年自动化一套都出来了。
个人感觉这个倒不是坏事,原来是“一台电脑一根烟,一个框架看一天”,现在AI能干的我躺着就好了,必要时按两下键盘再加个回车。
只是希望能够不忘初心吧,比赛归比赛,不要让AI这个“工具”吞噬了对未知探索的欲望~
最后是附件
https://peng1ai.lanzouv.com/iy1Yb40t4t4b
密码:cfm5
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:AI丽丝的人偶工坊 蓬莱人形 蓬莱人形《2026年山东省某市网络安全职业技能大赛(含题目、WP)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。







评论