# N1CTF Junior 2026 WriteUp
# Reverse 方向:
# MaybeAndroid
题目提供了一个 Android APK。安装后发现是一个 “Python 脚本运行器”,内置了几个 Python 脚本。 核心目标是获取 flag_check.py 中的 flag。直接运行 flag_check.py 会提示:“该脚本仅限 VIP 用户使用”,且该脚本是一个校验脚本,需要输入正确的 flag 才能通过,而不是直接输出 flag。
通过反编译 APK 定位到 MainActivity.kt

从而可以发现 app 的逻辑:Python 脚本存放在 Assets 目录;点击运行脚本时,会检查文件名。如果是 flag_check.py (VIP_SCRIPTS),则调用 VipManager.isVip (context) 检查权限;如果不是 VIP,弹出提示并拦截运行;脚本运行器界面是 read-only(只读)的 Text 组件,无法直接在 App 内修改代码。
为了运行脚本,首先需要成为 VIP。定位到 com.example.maybeandroid.VipManager 类,分析 activate 方法:

从而编写 Python 脚本解密 AES 密文:
1 | import base64 |
获得 VIP 激活码为:F4E52DFB41CCC32F8FFFC340A3804383
获得 VIP 后,可以查看到 flag_check.py 的源码:
1 | import sys |
保存文件后,使用 MT 管理器对 APK 进行 V1+V2 签名,卸载旧应用,安装修改后的使用 MaybeAndroid.apk,
最后输入激活码激活 VIP。打开脚本运行器,选择 flag_check.py 并运行,观察日志输出。

取前 32 位 MD5 哈希值得到:
flag{5f19b83de29bd46e9e02f7f88bfb4ea2}
# Microsoft VS Code
首先看到主逻辑:
1 | // Hidden C++ exception states: #wind=1 |
密文:

同时因为 CryptSetKeyParam 函数用于自定义会话密钥操作的各个方面。此函数设置的值不会保存到内存中,并且只能在单个会话中使用。(可能跟 AES 相关)
其实就是初始化密钥的,可以确定 iv:

key 不是写死在输入里,而是运行时拼出来的,原始 key 材料在 .data 里,0x14003C000,内容是 0x00..0x1F 共 32 字节,在这里 (sub_140008150) 会调用 sub_1400012F0 生成两个字符串
1 | // Hidden C++ exception states: #wind=4 |

会调用 build_registry_strings_gmp 生成两个字符串,然后 read_registry_value_16bytes 读取注册表
然后在 sub_140007980 这个函数打开了注册表读取 HKEY_USERS\SOFTWARE\Microsoft\Windows NT\CurrentVersion 下 ProductName 的前 16 字节

1 | void __fastcall sub_140007980(const WCHAR *a1, const WCHAR *a2, int a3, int a4, int a5, void *Dst) |
16 字节会 XOR 到 0x14003C010,而 g_dyn_bytes 实际上是 g_seed_bytes + 0x10,也就是 key 的后 16 字节。
最终 CryptImportKey 里把 g_seed_bytes(被 XOR 后)拷贝到 key_blob [12..],并设置 keylen=32,所以 AES-256 key 就是:
key [0..15] = g_seed_bytes [0..15] // 固定 00..0F
key[16..31] = g_seed_bytes[16..31] XOR ProductName[0..15
这里就是 key_blob

最后的 exp:
1 | from Crypto.Cipher import AES |
运行得:flag{M1cr0SOf7_V5_C0dE,d0_Y0U_Kn0W??}
# It's_Wizard_Time
首先运行程序得到

从而根据输出定位到函数 Il11:
1 | __int64 Il11() |
通过分析可知,iI1iii1l1ilIi(校验函数)和 1Il11ilIlI1iIii1(生成函数)是两个关键函数:
1 | __int64 __fastcall iI1iii1l1ilIi(__int64 a1, __int64 a2) |
可以看出这是一个经典的 “字符位置位图 “(Character Position Bitmap)校验逻辑,因此通过分析 iI1iii1l1ilIi 函数,可以完全还原出那 5 个正确的咒语字符串。
通过关键代码

可以知道程序计算了一个巨大的数组 v3。对于每一个字母(比如 'a'),它用一个 64 位的整数来记录这个字母在字符串中出现的所有位置。
最后,程序检查:
1 | if ( v3[26 * m + n] == I1iiIi[26 * m + n] ) |
这意味着 I1iiIi 这个全局数组里,存储了标准答案的位图。

所以 exp:
1 | def solve_magic_spells(): |
运行得到 5 条咒语

依次输入程序后得到

可以看出是 317427355F77317A3452645F37314D33,将其转为 ASCII 码是:1t'5_w1z4Rd_71M3
所以 flag{1t'5_w1z4Rd_71M3}
# find_my_time
首先通过搜索字符串定位到函数 sub_140002E80
1 | __int64 __fastcall sub_140002E80(QApplication *a1, int *a2, __int64 a3, int a4, __int64 a5, __int64 a6, char *a7) |
可以看出该函数创建 了 QApplication、QMainWindow,构造主控件 sub_14006C560,进入事件循环
1 | __int64 __fastcall sub_14006C560(QPixmap *a1, __int64 a2, __int64 a3, __int64 a4) |
sub_14006C560,主控件构造加载了资源图,创建 QTimer,连接 timeout 到 sub_14006C430
1 | void __fastcall sub_14006C430(QTimer *a1, __int64 a2, __int64 a3, __int64 a4) |
- GetSystemTimeAsFileTime → 计算 UTC 秒 ts
- 调用 sub_1400017C0 做时间判定 + 生成关键参数
- 调用 sub_140002160 根据判定结果更新内部字符串 / 状态
- QWidget::update 触发绘制
重点是 sub_1400017C0:时间 → 多重素数筛选
1 | __int64 __fastcall sub_1400017C0(__int64 a1, __int64 a2, unsigned __int64 a3, __int64 a4) |
把 FILETIME 转成 year,month,day,hour,min,sec
月份必须在表 xmmword_14007D230 + var_F8=11(即 {2,3,5,7,11})
日 / 时 / 分 / 秒要过 sub_140001540(素性检查)
YYYYMMDD、HHMMSS、YYYYMMDDHHMMSS 都要是素数
字符串里不能含字符 '0'(遍历 YYYYMMDDHHMMSS,若见 '0' 直接失败)
还要求:ts 是素数、YYYYMMDDHHMMSS||ts 和 ts||YYYYMMDDHHMMSS 也是素数
sub_140001540:把数字转十进制字符串 → sub_1400047F0 → GMP 的素性判定
sub_140002160:满足条件后才进入 “解密显示”
– sub_140001E70 /sub_140001EE0:从 .rdata 里 XOR 解出两段十进制密文
– 以 p=YYYYMMDDHHMMSS、q=p||ts、r=ts||p 组 RSA
– 以 e=ts 做 m = c^d mod n(内部用 GMP 大数函数)
– sub_140001F50 导出字节流,sub_1400015B0 生成可显示字符串(不可打印字节
变成 \xNN)
– 结果分别写到控件两个字符串槽位
sub_14006BD20:paintEvent,把两段字符串画到背景图上
通过后,构造三个大素数 p=YYYYMMDDHHMMSS、q=p||ts、r=ts||p,用 n=pqr、e=ts 对两个内置密文做 RSA 反解(密文是 XOR 过的十进制串)
结果字节流再转成可显示字符串,可以给出爆破 exp
还要满足一串素性检查:YYYYMMDD、HHMMSS、YYYYMMDDHHMMSS、timestamp、YYYYMMDDHHMMSS||timestamp、timestamp||YYYYMMDDHHMMSS 都要是素数
代码里还有一个字符串里不能含 0 的检查点(紧跟在 YYYYMMDDHHMMSS 的素性判断之
后)
- 月份表是 {2,3,5,7,11},但无 0 其实只剩 11 月。
- 解密密文需要原始密文数据,它在 exe 的 .rdata 中,经过 XOR 混淆:XOR key = 51 - dword_14017C040 通过函数 sub_140001E70 /sub_140001EE0 可知 key 只能让结果为数字串用脚本验证 key=51 才解出可用密文
- 用上述约束枚举时间 → 计算 p/q/r → RSA 解密 → 输出字符串
exp:
1 | import datetime |
运行得到:

最后的 flag 为:flag{4Kr0s5_Th0u54nDs_0f_yE4Rs_u_f1nd_m3_1nTh3_sE4_oF_7imE}