Seccon 2023
Seccon 2023
jumpout
发现一堆jump(xxx)
用了一些混淆通过jump rax ,jump rcx的方式,本题看了一下好像代码量并不大,直接看汇编即可
unk_4030是明文,unk_4010是估计异或的key
动调下就行了
1 | key = [0xF6,0xF5,0x31,0xC8,0x81,0x15,0x14,0x68,0xF6,0x35,0xE5,0x3E,0x82,0x9,0xCA,0xF1,0x8A,0xA9,0xDF,0xDF,0x33,0x2A,0x6D,0x81,0xF5,0xA6,0x85,0xDF,0x17] |
SECCON{jump_table_everywhere}
Sickle
通过b = pickletools.dis(payload)方法可以反汇编
下面这段是处理输入
1 | 0: \x8c SHORT_BINUNICODE 'builtins' |
下面是检查输入长度
1 | 228: \x8c SHORT_BINUNICODE 'builtins' |
另外注意到中间有pow,65537,大数可能跟RSA有关
注意到进行pow之前还有个xor操作
因此可以得到以下的代码,注意的是这种分析方法无法分析出xor_key = chunk,key是变化的,只能靠最后的猜测或者对密码的了解(因为不换key解出来的是seccon开头的后面却是乱码,可以猜测key变了)
XOR 所用的密钥并不是一成不变的。在加密完第一个数据块后,用于加密 下一个 数据块的密钥,会变成 上一个 数据块加密后的 密文 。这是一种经典的加密模式,叫做 密码块链接(CBC) ,它可以防止相同的明文块产生相同的密文块,从而提高安全性。
1 | from calendar import c |
Flag: SECCON{Can_someone_please_make_a_debugger_for_Pickle_bytecode??}
我们通过dis不能看到完整代码的原因在这
1 | 261: . STOP |
要把payload里在运行期永远跳不到的STOP字节先改掉,才能让pickletools把后面的字节继续“线性反汇编”。也就是说,做了一个离线分析用的补丁:把偏移261处的.
(STOP,0x2e)改成N
(NONE,0x4e),再次dis才看到第二段指令流;同理偏移330处再改一次,才能继续看到第三段。这样做只是为了让反汇编不中途停止,真实执行时之所以不会触发这些STOP,是因为前面构造了f.seek(...)
的“跳转”,反序列化过程会把流位置挪到别处,根本不会线性走到这些STOP。
1 | 118: \x8c SHORT_BINUNICODE 'seek' |
之后改完后还会报错
在这道 CTF 题里,出题人故意“滥用”了 pickle,重复写 memo[7] 来做某种控制流/数据流的技巧。真实执行时,Python 的 pickle.load
是允许覆盖的,不会出错;只是 dis 工具觉得“不规范”,所以停下来了。
改成如下这样即可
1 | # ... 省略了切片和 from_bytes 的指令 ... |
在第一次循环的结尾,硬编码的第一个密文 8215359690687096682 被存入了 memo[17] 。
1 | # ... 第二次循环开始,切片、转换字节 ... |
在第 701 行,程序没有像第一次那样去 GET 13 (获取初始密钥),而是去 GET 17 。而 memo[17] 里存的,正是上一次循环中用来做比较的那个 硬编码的密文 !
当然如果这个看不出来还可以再patch一下,打印出详细的堆栈信息。
1 | import importlib,io,pickle,pickletools |
Perfect-Blu
题目提供的是一个包含 BDMV 的蓝光镜像(.iso),用 VLC 打开后会出现一个“键盘式”的交互菜单,用来输入 flag
我本地还真没这个,ubuntu里有..
BDMV(Blu-ray Disc Movie)是Blu-ray光盘的一种文件格式,专门用于存储蓝光光盘上的视频内容,通常包含电影、视频和相关的多媒体文件。BDMV文件夹内包含多个子文件夹和文件,通常包括:
- BDMV:主文件夹,存储蓝光电影的相关数据文件。
- AACS:用于存储加密相关信息,用于保护Blu-ray内容不被非法复制。
- CERTIFICATE:用于存储数字证书文件,用于验证合法性和保护数据。
BDedit 0.40 / 0.55 捐赠免费下载 - VideoHelp
可以下载一个这个来打开
加载后点击菜单
load选择stream可以加载
- 菜单里每个按钮(比如数字、字母键)都有一个位置,用屏幕像素坐标 (x, y) 标识。
- 在蓝光的交互菜单里,这些坐标常用于定义“热点区域”或按钮中心点。
这是对应按钮被按下时触发的菜单脚本/命令(Call Object),通常用于跳转或执行某段逻辑。这里先不深究含义,重点是我们要把 (453, 672) 这个坐标映射成哪个“键”。
让我们快速编写一个辅助函数来将坐标转换为键盘字符:
1 | KEYS = [ |
参考Sickle (SecCon Quals 2023) - HackMD
xy怎么来的,多看几个格子的xy就能发现了,自己对照下
1 | import struct |
1 | SECCON{JWBH-58EL-QWRL-CLSW-UFRI-XUY3-YHKK-KFBV} |
差不多是下面这个意思
这个object的数和你其他不一样的就是特殊的,并且是第一个不同的,绿色那个XY范围超了也不行
1 | print(xy_to_key(453,672))可以验证下 |
1 | a = '1234567890QWERTYUIOPASDFGHJKL{ZXCVBNM_-}' |
xuyao
这个sbox看起来像AES的S盒,密文在下面,上面还有一个finsh和cat,目前不清楚作用
main函数:
- mmap 0x1000 临时页(addr),构造两套“描述符表”(v20+v21),每个条目形如 {ptr=addr, off=i, size=1/2/4/8};
- 读入 Message,逐字节写入 addr+off,并把已写字节数记到 [addr+224];
- 计算填充到 16 字节边界,把总长度写入 [addr+228],用 [addr+236] 的 pad 值把 [len..len+pad-1] 填为 pad;
- 调用 encrypt(v20, addr, a3=0x4000000E4(高32位=4,低32位=228), key=”SECCON CTF 2023!”, v21 对 addr 中的明文做 128-bit 分组加密(ECB)并把密文写入 v21;
- 遍历 v21 指向的每个字节与全局 enc[] 逐一比较,全部相等打印 “Correct!”,否则 “Wrong…”
加密顶层 custom_encrypt(原 encrypt,已注释)
- 参数:若干描述符表(r_descA/r_descB/out_desc)、输入基址与长度、固定16字节ASCII密钥。
- 初始化子密钥材料:将密钥按32位分段,配合 INIT_KEY_XOR 做异或与字节序处理,获得初始轮密钥(还会叠加 ROUND_CONST)。
- 进行64轮的子密钥演化,每轮通过 key_schedule_apply(见下)进一步混合。
- 以128-bit为单位对输入做分组,逐块调用 encrypt_block_128 完成轮函数迭代。
非线性与线性层key_schedule_apply
- sub_bytes(原 tnls,已注释):对32位输入的每个字节,分别用 SBOX 做替换,按字节拼回32位。
- linear_mix(原 els,已注释):L(x) = x ^ rol3(x) ^ rol14(x) ^ rol15(x) ^ rol9(x),提供强扩散。
- sub_bytes_then_linear_mix(原 sub_555…,已注释):F(x) = linear_mix(sub_bytes(x)),组合成轮函数的核心。
分组加密 encrypt_block_128(原 encrypt_block,已注释)
- 读取4个32位字(注意每个字在读写时做了 bswap32,确保统一的字节序)。
- 循环64轮:每轮调用一次 feistel_round,并对4个字做循环置换(四路Feistel风格的扩散)。
- 写回时再次对4个字做 bswap32。
轮函数 feistel_round(原 r,已注释)
- 从状态中取3个32位字做 XOR 汇总,结果再与本轮子密钥 XOR。
- 将上述32位值送入 F 函数:sub_bytes_then_linear_mix(见下)得到非线性+线性扩散后的结果。
- 把该结果 XOR 回目标字(实现Feistel式的“混入”),完成一轮。
子密钥演化
- key_schedule_apply(原 ks,已注释):先做 sub_bytes,再做 rotate_xor_mix,得到新一轮子密钥。
- rotate_xor_mix(原 kls,已注释):K(x) = x ^ rol11(x) ^ ror7(x),线性可逆、实现轻量扩散。
- 初始子密钥由 “SECCON CTF 2023!” 与 INIT_KEY_XOR 的分段/字节序混合得到,并在轮间叠加 ROUND_CONST。
但是还是不太懂,得先学学AES和DES,还有SM4,RC4了
首先发现的这个SBOX是AES的SBOX,但是他在这里做了变换,起混淆作用可能
这里的addr什么的内存操作主要起混淆作用
1 | # 你的 SBOX:这里给一个占位(恒等映射),实际请替换成程序里的全局 SBOX 内容(长度 256) |
简化下就是下面这样
1 | def tnls(x): |
kls呢,rotate_xor_mix:K(x) = x ^ rol11(x) ^ ror7(x),线性可逆、实现轻量扩散。
这个也非常简单,对 32 位输入值进行一些异向和旋转。
1 | def kls(x): |
这段循环是在“按轮生成子密钥并做4字寄存器轮换”的关键流程,ROUND_CONST 就是每一轮参与异或的“轮常量”,用于打破轮与轮之间的对称性,避免出现滑移/对称弱点,让每一轮的输入都不同。
1 | v16 = &ROUND_CONST; |
这个循环有什么用呢?
把变量抽象成人话(用 4 个 32 位字寄存器表示当前状态):
设当前 4 个 32 位状态字为 W0, W1, W2, W3(分别对应 v17、v19、v21、v22 指向的位置,具体是由前面“基地址+偏移”的描述符指向的内存)。
v16 从 &ROUND_CONST 开始,每次循环自增一次,相当于每轮取一个 32 位轮常量 RC = *v16。
addr[…] 是函数里的一块临时工作区(堆栈/映射区),拿来放中间值;v12 指向了“输出描述符数组”,用于把该轮的结果写出去(作为 round key 或本轮输出)。
循环每一轮的逻辑可以压缩成下面这几步:
取 3 个字相加(按位异或)
- tmp = W0 ^ W1
- tmp = tmp ^ W2
注入轮常量
- tmp = tmp ^ RC:作用:让每轮输入都有“回合编号”的差异,打破对称。
经过一个非线性+线性扩散的变换
y = key_schedule_apply(tmp)
其中 key_schedule_apply 等价于“sub_bytes 再 rotate_xor_mix”,也就是先对 tmp 按字节用 SBOX 做替换,再做线性旋转异或扩散(ROL11、ROR7 异或),产生强非线性和扩散。
与第4个字混合,得到本轮输出字 new_word
new_word = W3 ^ y
C 里对应 v23 = addr[12] ^ v22; 然后把 v23 写回 addr[11] 和目标位置(通过 v12 指向的描述符: (_DWORD *)(*v12 + *((unsigned int *)v12 + 2)) = v23)。
这一步就是“Feistel风格”的把 F(tmp) 混入 W3。
4 字寄存器轮换(把新字注入进来)
(W0, W1, W2, W3) ← (W1, W2, new_word, W0)
对应代码里的:
*v22 = *v17; // W3 = old W0
*v17 = *v19; // W0 = old W1
*v19 = *v21; // W1 = old W2
*v21 = addr[11]; // W2 = new_word这是一种“四路Feistel/轮换寄存器”的常见扩散策略。
因此,这个 do { … } 循环就是“按轮常量 RC 驱动”的轮迭代:
组合前三个字异或,再异或本轮 RC;
过一层非线性+线性(key_schedule_apply);
与第四个字异或得到本轮输出字,既写回(或存到某个输出表)也更新状态;
做一个 4 字的循环置换,把 new_word 放入状态,准备下一轮;
v16++ 取下一个 ROUND_CONST,v12++ 把“该轮输出”写到下一位置。
ROUND_CONST 的作用总结是每一轮独有的常量,按位异或注入到“W0^W1^W2”的组合中。
目的:
- 打破轮与轮完全相同的结构导致的对称/滑移(slide)弱点;
- 保证即使状态在某些情况下相同,每轮的输入仍然不同;
- 提升密钥扩展/轮函数的不可预测性和抗分析性。
可以把它类比为 AES 里每轮加的 Rcon,或是很多自定义分组密码/哈希里常见的“轮常量”
它把固定主密钥(ASCII 常量)变成一串“每轮不同”的子密钥,这串子密钥随后被 encrypt_block_128 在 64 轮中逐轮取用,直接决定了加密输出。
这段循环在整个程序里承担的是“按轮生成子密钥(round keys)并更新4字寄存器状态”的核心职责。通俗说:它就是定制分组密码的“密钥扩展/轮密钥调度”环节,决定每一轮用什么子密钥参与加密。
把它放回整体流程中的位置
- 在 custom_encrypt 中,先用固定 ASCII 密钥 “SECCON CTF 2023!” 和 INIT_KEY_XOR 混合,得到4个32位的初始状态字(你可以理解为 K0..K3)。
- 紧接着就是你看到的这段循环:它会迭代多次(64次),每一轮从这4个状态字推导出一个32位的“本轮子密钥”,同时更新这4个状态字,为下一轮准备。
- 生成出的子密钥序列被写到一张“输出描述符/表”里(v12 指向的地方),随后 encrypt_block_128 在真正加密数据时,会一轮一轮取用这些子密钥。
encrypt_block(encrypt_block_128)
简而言之,它接收一个数据块(4 个 32 位值)、32 个 32 位子键和输出指针,交换字节顺序并为每个子键执行 32 轮调用函数
feistel_round 是每一轮的“F-函数混入”步骤:把三个32位字按位异或后再异或子密钥,过一层“字节S盒替换 + 线性扩散”,然后把结果异或到第四个字上。它本身不做寄存器轮换,四字轮换是在外层 encrypt_block_128 里做的。
更具体一点:
设本轮状态为四个32位字 (W0, W1, W2, W3),子密钥为 K。
计算 tmp = W0 ^ W1 ^ W2 ^ K。
计算 F(tmp) = linear_mix(sub_bytes(tmp)):
- sub_bytes:对 tmp 的每个字节用 SBOX 做查表替换(逐字节)。
- linear_mix:把输入做多个 32 位循环左移后按位异或,形成线性扩散。
把 F(tmp) 混入 W3:W3’ = W3 ^ F(tmp)。
返回 W3’。外层随后会做四字轮换 (W0,W1,W2,W3) → (W1,W2,W3’,W0)。
我们需要先关注feistel_round 里的这个函数
1 | def els(x): |
这是linear_mix的逻辑
那么feistel的逻辑就是
1 | def r(block, subkey): |
因此我们有encrypt_block的逻辑
1 | def encrypt_block(block, subkeys): |
这样综合来看跟哪个算法很像?SM4,但是S盒好像不太一样
我们可以编写一个 函数来解密:decrypt_block
1 | def decrypt_block(block, subkeys): |
我们不知道的是subkeys
动调出来:
可以在此处下断点
或者在这,然后a2点进去向下找就行
1 | from Crypto.Util.Padding import unpad |
1 | b'Congratulations! You have decrypted the flag: SECCON{x86_he2_zhuan1_you3_zi4_jie2_ma3_de_hun4he2}\n' |
Optinimize
是一道NIM逆向题,可读性很差
Nim 是一门开源的静态类型编程语言,可以编译成 C 或 JavaScript,具有 Python 的表现力和 C 的速度。Nim 支持引用、垃圾回收、宏、模板等特性,适合开发
运行后输出flag会逐渐变慢,题目名跟优化有关,可能我们就是要优化这个打印Flag的过程
程序每一轮会取一个目标计数 n,找出“第 n 个满足 p(t) % t == 0 的 t”,然后取 t % 256,并与一个按位异或表的对应元素异或,输出一个字节。
之所以越跑越慢,是因为 p(t)(整数拆分函数)增长极快,且每一轮都要从 0 重新枚举 t 去找第 n 个命中,使用大整数运算,没有缓存,复杂度爆炸
主流程与关键函数(已重命名)主打印循环函数: print_flag_loop
(原 NimMainModule,地址 0x11c30)循环变量 idx 从 0 到 38(总计 39 次输出)。
每轮:
- 读取目标命中次数 n = target_counts[idx](全局数组)。
- 把 n 以大整数形式传入 Q 函数,得到一个大整数结果 T(详见 Q 函数解释)。
- 计算 T mod 256(以大整数 mod 后,再通过 bigint_to_uint64_checked 转成无符号整数)。
- 与 xor_key_table[idx] 异或,得到一个 0..255 的字节。
- 输出该字节;循环直到 idx > 38。
已在循环开始位置添加注释“主循环开始:for idx in 0..38,每轮计算并输出一个flag字符”
find_nth_m_where_Pm_mod_m_eq_0: 给定目标计数 n,t 从 0 递增;每轮计算 P(t) 并判断 P(t) % t == 0;若成立则命中计数+1;当命中计数==n 时返回 t(以大整数形式写回 a1)
计数搜索函数: find_nth_m_where_Pm_mod_m_eq_0
(原 Q__main_u13,地址 0x11740)
输入:目标命中次数 n(以大整数形式)。
逻辑(高层等价):
令 count = 0,t = 0。
do:
- t = t + 1
- 计算 p(t) = compute_P_of_n(t)(大整数)。
- 若 p(t) % t == 0,则 count += 1。
当 count 达到 n 时,返回该 t(以大整数写回到调用方的大整数缓冲)。
代码里能清晰看到:
- 通过 plus 把 t 自增;
- 调用 compute_P_of_n 得到大整数;
- 调用 mod(p(t), t);
- 用 eqeq 与 0 比较,命中就把计数大整数 v55 自加 1;
- 用 lt(v55, n) 判断是否已满足目标;不小于 n 时,搬运当前 t(v57/v58)作为返回值,结束循环。
1 | def find_nth_m_where_Pm_mod_m_eq_0(n): |
compute_P_of_n :
compute_P_of_n: 递归-迭代混合的大整数多项式运算,包含 plus/minus/mod/lt/eqeq 等;核心是生成某序列 P(n)
这是“p(n)”的计算器,使用大量大整数操作 plus/minus/lt/eqeq/mod,形态上符合欧拉五边形数递推(generalized pentagonal numbers)实现的整数拆分函数 p(n):
反复初始化 302 作为常用常量;
内层循环按条件加减之前的项,按 lt 条件退出;这与五边形数递推的“+ + - - …”累加模式吻合;
结果以大整数形式返回。
大整数到整数转换:
bigint_to_uint64_checked
(原 toInt__main_u70,地址 0x10e30)负责把大整数安全地转换成 64 位无符号整数,有专门的负号和 64 位拼接处理分支(见对 a8+8、a8+12 两个 32 位字段的操作)。
主循环里用它把“(T mod 256)”转成普通整数再异或输出。
1 | def P__main_u4(x): |
主循环:
for idx in 0..38:
- n = target_counts[idx]
- t = find_nth_m_where_Pm_mod_m_eq_0(n)
- c = (t mod 256) // 大整数取模 256
- out_ch = c XOR xor_key_table[idx]
- print byte(out_ch)
最后还有个异或逻辑
下面这个是代码的逻辑
1 | def P(i: int): |
后面就是优化
现在我们知道了函数的作用
生成几个元素给了我们序列 。快速谷歌搜索显示它是 Perrin sequnce。P__main_u4``3, 0, 2, 3, 2, 5, 5, 7, 10, 12, 17, 22, 29, 39, ...
它有一个适用于所有素数的属性。我们可以看到它正是在检查的内容,因此可以得出结论,它返回了 -th Perrin 素数。n|p(n)``Q__main_u13``n
exp:
1 | import gmpy2 |
1 | SECCON{3b4297373223a58ccf3dc06a6102846f} |