跳至主要内容

FurryCTF2026

Reverse

签到题

  • 丢进DIE查了下
  • 发现有VMP的壳子,直接找工具脱掉(之前正好参加了Lilac CTF,官方wp中提到了个脱壳工具,在这正好用上了)
  • 再丢进IDA
    截图 202602011856

未来程序

  • 有个c++的源码,因为不会c++,所以让ai协助我理解了下代码
  • 发现encoder.txt,把内容按源码改了改
  • 简单了解了下源码
  • 把output还原成输入内容后发现程序卡死
  • 没办法了找ai做个脚本进行还原
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys

def bits_to_bytes(bitstr: str) -> bytes:
"""把 '010101...' 转成 bytes(左侧补 0 到 8 的倍数)"""
bitstr = bitstr.strip()
if not bitstr:
return b""
pad = (-len(bitstr)) % 8
bitstr = ("0" * pad) + bitstr
return bytes(int(bitstr[i:i+8], 2) for i in range(0, len(bitstr), 8))

def parse_output_line(line: str):
"""
解析 Output=<D>|<S>
D = A-B (binary)
S = A+B (binary)
"""
line = line.strip()
if "=" not in line or "|" not in line:
raise ValueError("格式不对,应为:Output=<bits>|<bits>")
payload = line.split("=", 1)[1]
d_str, s_str = payload.split("|", 1)
return d_str.strip(), s_str.strip()

def main():
# 允许:从 stdin 读;或把 Output=... 当作参数传进来
if len(sys.argv) >= 2:
line = sys.argv[1]
else:
line = sys.stdin.readline()
if not line:
print("请提供一行 Output=...|...", file=sys.stderr)
sys.exit(1)

D_str, S_str = parse_output_line(line)

D = int(D_str, 2)
S = int(S_str, 2)

# A = (S + D)/2, B = (S - D)/2
if S < D:
raise ValueError("S < D:不符合非负 B 的假设")
if ((S + D) & 1) or ((S - D) & 1):
raise ValueError("奇偶性不对:S±D 不能整除 2")

A = (S + D) // 2
B = (S - D) // 2

# 把 A、B 转回字节串(ASCII)
A_bytes = bits_to_bytes(format(A, "b"))
B_bytes = bits_to_bytes(format(B, "b"))

try:
A_txt = A_bytes.decode("ascii")
B_txt = B_bytes.decode("ascii")
except UnicodeDecodeError:
# 如果不是 ASCII,就给出原始 bytes 方便你自己再处理
print("A_bytes =", A_bytes)
print("B_bytes =", B_bytes)
sys.exit(0)

flag = A_txt + B_txt
print(flag)

if __name__ == "__main__":
main()

ezvm

  • ida分析后可以直接看到个假flag
  • 注意到n976364816
  • 976364816 = 0x3A322510
  • 也就是把2或c换为1
  • 结果就为POFP{317a614304}

Lua

  • 打开源码
  • 看到了一个base64表
  • 还有一长串的字符串
  • 丢到赛博厨子看下

截图 202602021253

  • 发现了关键的一行数字
  • 因为给了flag头是POFP,但是发现这样异或行不通
  • 试了flag为flag才可以得到正确的flag
  • 让这串数字异或122
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def solve_flag():
cipher = [0, 30, 19, 21, 9, 39, 45, 0, 45, 62, 7, 70, 38, 45, 63, 70, 1, 6, 65, 32, 83, 15]


flag = ""

flag += chr(cipher[0] ^ 102)

key = 114 # 'r'
for val in cipher[1:]:
flag += chr(val ^ key)
print(f"解密结果: {flag}")

if __name__ == "__main__":
solve_flag()

RRRacket

  • 这道题试了很多方法
  • 用了反编译raco也出不来正常的代码
  • 搜了很多东西也没整明白怎么正确解开
  • 我能用记事本打开zo文件才能看见key和密文
  • 用ai分析下怎么解密
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.Cipher import ARC4

KEY_STR = "pofpkey"

target_hex = "d31fa2c26c024feddef9b38853790c00285e367b916d49a111bfc2bcfb74"

def solve():
key = KEY_STR.encode('utf-8')
ciphertext = bytes.fromhex(target_hex)
cipher = ARC4.new(key)
decrypted = cipher.decrypt(ciphertext)
flag = decrypted.decode('utf-8')
print(f"{flag}")



if __name__ == "__main__":
solve()

XOR

  • 第一个坑就是这个头文件是坏的,需要修一下
  • 打开ida分析后发现多次出现NUITKA字样
  • 应该是NUITKA打包,所以用工具提取一下
  • 提取之后发现script.dll为主要内容
  • 丢到ida在rsrc段发现主要内容
  • 在rsrc段发现了加密的flag和key
  • ai帮我们提取一下并写脚本
1
2
3
4
5
6
7
8
# 提取到的原始数据
enc_flag = [122, 101, 108, 122, 81, 88, 25, 92, 25, 88, 89, 27, 68, 77, 117, 27, 89, 117, 76, 95, 68, 11, 87]
key = 42

# 执行异或运算并转换字符
flag = "".join(chr(x ^ key) for x in enc_flag)

print(f"解密后的 Flag 为: {flag}")

TimeManager

  • 发现不是exe程序
  • 丢到die分析一下
  • 发现是个需要linux环境下运行的程序
  • ida中完全呈现了加密方式
  • 找ai写个脚本在linux环境下运行即可(主要是随机方式不同)
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
import ctypes
import sys

try:
if sys.platform == "darwin": # macOS
libc = ctypes.CDLL("libc.dylib")
else:
libc = ctypes.CDLL("libc.so.6")
except OSError:
print("[!] 错误: 无法加载 libc。")
print(" 请在 Linux 环境 (如 Kali, Ubuntu, WSL) 下运行此脚本。")
print(" Windows 的 rand() 算法与题目不兼容,会导致解密失败。")
sys.exit(1)


DWORD_6043 = 0x0BEADDEEF

cipher = [
0x21, 0x71, 0xD8, 0xED, 0xDD, 0xA9, 0xCB, 0x02, 0xFB, 0x3E, 0x77, 0xDF, 0x96, 0x6D, 0x6D, 0x29,
0x69, 0xCF, 0xDC, 0xC1, 0xEA, 0xBE, 0x23, 0xAA, 0x1D, 0xE4, 0x25, 0xD4, 0x9D, 0x3A, 0x8A, 0x50,
0xCA, 0xD6, 0x86, 0x48, 0x21, 0xFB, 0xD5, 0x75, 0x44, 0x49, 0x63, 0x1B, 0x30, 0xB8, 0x18, 0x39,
0x22, 0xB2, 0x43, 0xC8, 0x82, 0x06, 0xDC, 0x1D, 0x88, 0xBF, 0x1A, 0xB8, 0x0C, 0xFB, 0x54, 0xC9,
0x57, 0x7A, 0xB3, 0xDD, 0x94, 0x70, 0x06, 0xAD, 0x41, 0x8F, 0x13, 0x7B, 0x66, 0x31, 0x90, 0xF7,
0xEC, 0xDC, 0xB7, 0xE8, 0xC4, 0x60, 0x3C, 0x69, 0xBD, 0xD8, 0x8E, 0x9B, 0xAB, 0xA0, 0x50, 0x07,
0xCD, 0x40, 0x7C, 0xFE, 0x30, 0xF2, 0xCA, 0x45, 0xE2, 0x53, 0x7D, 0x19, 0xD8, 0x16, 0x79, 0xBD,
0x47, 0xD3, 0x93, 0x33, 0xCD, 0xCB, 0xD4, 0xCA, 0xDE, 0x38, 0xB5, 0xC5, 0x36, 0xFF, 0xA3, 0x87
]

for i in range(10800):

seed = (DWORD_6043 + i + 1) & 0xFFFFFFFF

# 设置随机数种子
libc.srand(seed)
r1 = libc.rand()


cipher[i % 128] ^= (r1 & 0xFF)
cipher[i % 17] ^= (r2 & 0xFF)

flag_str = ""
for char_code in cipher:

if char_code == 0:
break
flag_str += chr(char_code)

print("\n[*] 解密完成!")
print("-" * 30)
print(flag_str)
print("-" * 30)

vmmm

  • 丢到ida发现反编译不了
  • 根据报错把idata改下名后保存db重新加载一遍就好了
  • 轻松定位到sub_40163B()函数
  • 观看完内部函数后发现该程序就为个loader
  • 主要在program.bin
  • 因为把bin内容整成反编译复杂,所以就丢给ai了
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
import struct

data = open("data.bin","rb").read()

key = list(data[0x300:0x300+16])
expected = [data[0x200 + 4*i] for i in range(32)]

def ksa_variant(key):
S = list(range(256))
j = 0
for i in range(256):
j = (4*j + S[i] + key[i % 16]) & 0xFF
S[i], S[j] = S[j], S[i]
return S

def prga(S, n):
i = j = 0
out = []
for _ in range(n):
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
S[i], S[j] = S[j], S[i]
t = (S[i] + S[j]) & 0xFF
out.append(S[t])
return out

S = ksa_variant(key)
ks = prga(S, 32)
flag = bytes([expected[i] ^ ks[i] for i in range(32)])
print(flag.decode())

  • 这道题我很好奇官方wp怎么写,因为我不用ai写不出来

Mobile

涩图大赏.apk

  • 经过一番”友好”的分析
  • 给我分析红温了
  • 终于发现涩图大赏(修复版)\assets\res\data.json
  • 有着Q1和Q3的答案
  • 那么Q2呢,我试了多次反编译,搜索了很多资料
  • 看了libluajava.so
  • 依旧没有反编译成功
  • 本来是能蒙对的,到最终flag出现的时候我才想起来我当时提交的upload.php的u是小写的!
  • 气死我了
  • 要不就能有血了
  • 最终的Upload.php还是在我不断烦ai才得到的
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
from __future__ import annotations
import argparse
import os
import sys
import zlib
import subprocess
from pathlib import Path
from typing import Optional, Tuple, List

# ---------------------------
# 1) '=' 自定义 base64 解码
# ---------------------------

def _build_b64_map() -> dict[int, int]:
"""
复现你反编译里 dword_10010 的作用:把 '+'(43) 到 'z'(122) 映射到 0..63,'=' 映射到 -2。
未在表内/非法字符:-1(会被跳过,类似允许空白/换行)。
"""
alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
m = {}
for idx, ch in enumerate(alphabet):
m[ch] = idx
m[ord('=')] = -2
return m

_B64_MAP = _build_b64_map()

def custom_b64_decode(buf: bytes) -> bytes:
"""
输入必须以 '=' 开头;该 '=' 不参与解码,而是“首 sextet 固定为 7”(你反编译里 v60=7)。
其余字符按标准 base64 表解码;非法字符跳过;padding 必须满足对齐约束。
"""
if not buf or buf[0] != 0x3D:
raise ValueError("custom_b64_decode: input does not start with '='")

out = bytearray()
p = 0
n = len(buf)

def next_sextet(is_first: bool) -> Tuple[Optional[int], bool]:
"""
返回 (val, advanced):
- val: 0..63, -2(padding), None(到末尾)
- advanced: 是否消耗了输入字符(用于 first sextet 特例)
"""
nonlocal p
if is_first:
# 反编译中:n2_3==0 时直接 v60=7,不读 buf[0]
p += 1
return 7, True

while p < n:
ch = buf[p]
p += 1
v = _B64_MAP.get(ch, -1)
if v == -1:
# 非法字符:跳过(反编译里 *(&v60 + slot)=-1 然后 ++n2_3 continue)
continue
return v, True
return None, False

first = True
while True:
sextets: List[int] = []
pad = 0
# 收集 4 个 sextet
while len(sextets) < 4:
v, _ = next_sextet(first)
first = False
if v is None:
# 正常结束:不允许残缺 quartet
if sextets:
raise ValueError("custom_b64_decode: truncated quartet")
return bytes(out)
sextets.append(v)
if v == -2:
pad += 1

a, b, c, d = sextets
if pad == 0:
if a < 0 or b < 0 or c < 0 or d < 0:
raise ValueError("Invalid base64 text (illegal sextet)")
v = (a << 18) | (b << 12) | (c << 6) | d
out.append((v >> 16) & 0xFF)
out.append((v >> 8) & 0xFF)
out.append(v & 0xFF)
elif pad == 1:
# 反编译:要求 d == -2 且 (c & 3) == 0
if d != -2 or c < 0 or (c & 3) != 0 or a < 0 or b < 0:
raise ValueError("Invalid base64 text (pad=1 constraints)")
# 2 bytes: (a<<10) | (b<<4) | (c>>2)
v = (a << 10) | (b << 4) | (c >> 2)
out.append((v >> 8) & 0xFF)
out.append(v & 0xFF)
return bytes(out) # padding 出现后应当结束
elif pad == 2:
# 反编译:要求 d == -2 且 c == -2 且 (b & 0xF) == 0
if d != -2 or c != -2 or b < 0 or (b & 0xF) != 0 or a < 0:
raise ValueError("Invalid base64 text (pad=2 constraints)")
# 1 byte: (a<<2) | (b>>4)
out.append(((a << 2) | (b >> 4)) & 0xFF)
return bytes(out)
else:
raise ValueError("Invalid base64 text (pad>2)")

# ---------------------------
# 2) 0x1C:前缀 XOR + zlib
# ---------------------------

def prefix_xor_decode(data: bytes) -> bytes:
out = bytearray(len(data))
acc = 0
for i, b in enumerate(data):
acc ^= b
out[i] = acc
return bytes(out)

def decode_0x1c_protected(buf: bytes) -> bytes:
if not buf or buf[0] != 0x1C:
raise ValueError("decode_0x1c_protected: not starting with 0x1C")
x = bytearray(prefix_xor_decode(buf))
x[0] = 0x78 # 强制 zlib 头第 1 字节
try:
y = zlib.decompress(bytes(x))
except zlib.error:
# 某些样本可能包含尾随垃圾:用 decompressobj 容忍
d = zlib.decompressobj()
y = d.decompress(bytes(x)) + d.flush()
y = bytearray(y)
if y:
y[0] = 0x1C
return bytes(y)

# ---------------------------
# 3) 0x1B 非 'L':XOR 流解密
# ---------------------------

def deobfuscate_0x1b_nonlua(buf: bytes) -> bytes:
"""
根据你贴的 NEON 分支,可等价为:
对 k=1..n-1:
buf[k] ^= ((k*n) % 255)
其中 n = len(buf)(反编译里用 v35 = dup(n2))
"""
if len(buf) < 2 or buf[0] != 0x1B or buf[1] == ord('L'):
return buf
n = len(buf)
out = bytearray(buf)
for k in range(1, n):
out[k] ^= (k * n) % 255
return bytes(out)

# ---------------------------
# 4) 总入口:自动识别
# ---------------------------

def unpack_bytes(buf: bytes) -> bytes:
if not buf:
return buf
b0 = buf[0]
if b0 == 0x3D: # '='
raw = custom_b64_decode(buf)
if raw and raw[0] == 0x1C:
return decode_0x1c_protected(raw)
return raw
if b0 == 0x1B:
raw = deobfuscate_0x1b_nonlua(buf)
return raw
if b0 == 0x1C:
return decode_0x1c_protected(buf)
return buf

def to_standard_luac(buf: bytes) -> Optional[bytes]:
"""
若是 0x1C 'Lua'(即 1C 4C 75 61),转换成标准 0x1B 'Lua'。
"""
if len(buf) >= 4 and buf[0] == 0x1C and buf[1:4] == b"Lua":
out = bytearray(buf)
out[0] = 0x1B
return bytes(out)
# 也可能已经是标准 0x1B 'Lua'
if len(buf) >= 4 and buf[0] == 0x1B and buf[1:4] == b"Lua":
return buf
return None

def run_unluac(unluac_jar: Path, inp_luac: Path, out_lua: Path) -> Tuple[bool, str]:
cmd = ["java", "-jar", str(unluac_jar), "-o", str(out_lua), str(inp_luac)]
try:
p = subprocess.run(cmd, capture_output=True, text=True)
except FileNotFoundError:
return False, "java not found; please install Java runtime"
if p.returncode != 0:
return False, (p.stderr or p.stdout or "").strip()
return True, (p.stdout or "").strip()

def main():
ap = argparse.ArgumentParser(description="Unpack custom lua chunks and optionally decompile to .lua")
ap.add_argument("inputs", nargs="+", help="input files")
ap.add_argument("-o", "--outdir", default="out", help="output directory")
ap.add_argument("--unluac", default=None, help="path to unluac.jar (optional)")
args = ap.parse_args()

outdir = Path(args.outdir)
outdir.mkdir(parents=True, exist_ok=True)

unluac_jar = Path(args.unluac).resolve() if args.unluac else None
if unluac_jar and not unluac_jar.exists():
print(f"[!] unluac.jar not found: {unluac_jar}", file=sys.stderr)
unluac_jar = None

for inpath_str in args.inputs:
inpath = Path(inpath_str)
data = inpath.read_bytes()

unpacked = unpack_bytes(data)
dec_path = outdir / (inpath.name + ".dec")
dec_path.write_bytes(unpacked)
print(f"[+] wrote: {dec_path} ({len(unpacked)} bytes)")

std = to_standard_luac(unpacked)
if std:
luac_path = outdir / (inpath.name + ".luac")
luac_path.write_bytes(std)
print(f"[+] wrote: {luac_path} (standard luac header)")

if unluac_jar:
lua_path = outdir / (inpath.stem + ".lua")
ok, msg = run_unluac(unluac_jar, luac_path, lua_path)
if ok:
print(f"[+] decompiled: {lua_path}")
else:
print(f"[!] unluac failed for {inpath.name}: {msg}", file=sys.stderr)
else:
print(f"[-] {inpath.name}: not recognized as Lua bytecode (no 1C/1B 'Lua' header after unpack)")

if __name__ == "__main__":
main()

关于本文

由 GuQing 撰写,采用 CC BY-NC 4.0 许可协议。