
Re方向的题也是AK了喵(骄傲),队伍里面其他同学也是非常厉害,总之这次比赛还是收获蛮大的,以赛代学真的能学到不少东西。
接下来就是本次题目的一些wp及其讲解,有的过于简单的我就不详细说了。
ezzz_math
这道题属于是签到题,程序拖到ida中发现有一个方程组,将其解开之后即可获得flag。
ezpy和ELF
这两道题都是简单的python逆向题目,python逆向题跟我之前的文章方法都差不多,一般套路是python反编译+加密算法,这种也都是板子题非常简单。
其中ezpy反编译后是RC4加密(这个就不说了),而ELF关键逻辑还原成可读代码如下:

逆向思路
- 输入长度固定 24,被分成 8 个 3 字节块
- 每块会 base64 后取 md5.hexdigest()(32 字符)
- Rep 只是用固定种子 161 对 32 字符串做 Fisher–Yates 洗牌,所以每次的置换是恒定的,可提前算出逆置换
- 给定的 flag 长度 256 = 8×32,可按 32 分块,每块逆洗牌即可还原原始 md5 值
- 每个原始块只有 3 字节,可在可打印 ASCII 范围内暴力枚举 product(charset, repeat=3),找到满足 md5(base64(block)) == target_md5 的明文
MysteriousStream
一开始发现文件有两个,一个是challenge还有一个是payload.dat。
那么我们先用exeinfo查看一下文件类型:

发现没有加壳,那么我们直接拖到ida中进行分析。
找到了main函数,并对其进行反编译:
int __fastcall main(int argc, const char **argv, const char **envp){ FILE *v3; // rax FILE *v4; // r12 __int64 v5; // r14 char *v6; // rax char *v7; // rbp size_t v8; // r13 __int64 i; // rcx char v11[17]; // [rsp+7h] [rbp-41h] BYREF unsigned __int64 v12; // [rsp+18h] [rbp-30h]
v12 = __readfsqword(0x28u); v3 = fopen("payload.dat", "rb"); if ( v3 ) { v4 = v3; fseek(v3, 0LL, 2); v5 = ftell(v4); if ( v5 < 0 ) { puts("Get file size failed"); fclose(v4); return 1; } else { fseek(v4, 0LL, 0); v6 = (char *)malloc(v5); v7 = v6; if ( v6 ) { v8 = fread(v6, 1uLL, v5, v4); fclose(v4); if ( v5 == v8 ) { qmemcpy(v11, "P4ssXORSecr3tK3y!", sizeof(v11)); rc4_variant(v7, v8, &v11[7], 10LL); if ( v8 ) { for ( i = 0LL; i != v8; ++i ) v7[i] ^= v11[i % 7]; } __printf_chk(1LL, "Result: %s\n", v7); free(v7); return 0; } else { __printf_chk(1LL, "Read failed! Expected %ld bytes, got %zu bytes\n", v5, v8); free(v7); return 1; } } else { puts("Malloc memory failed"); fclose(v4); return 1; } } } else { puts("payload.dat not found"); return 1; }}这段 main 的核心作用就是从 payload.dat 读入一段数据,然后用”两层解密/反混淆”还原成字符串并打印。
也就是:payload.dat 的内容 → 先做 RC4 变种处理(key=“Secr3tK3y!”)→ 再做重复 XOR(key=“P4ssXOR”)→ 得到字符串输出。
payload.dat用ida打开发现只有下面 40 字节,因此这就是密文。

所以写一个python解密脚本就可以了:
enc_hex = "f1c652acab33ee6873cea53f0e0eb7fdc731be9aa7e8d41fe04b3154ff7cccd2160b4034e6b815bf"enc = bytes.fromhex(enc_hex)xor_key = b"P4ssXOR"rc4_key = b"Secr3tK3y!"
#### XORtmp = bytes(b ^ xor_key[i % len(xor_key)] for i, b in enumerate(enc))
#### RC4 变体S = list(range(256)); j = 0for i in range(256): j = (j + S[i] + rc4_key[i % len(rc4_key)] + (i & 0xAA)) & 0xFF S[i], S[j] = S[j], S[i]i = j = 0out = bytearray(tmp)for k in range(len(out)): i = (i + 1) & 0xFF j = (j + S[i]) & 0xFF S[i], S[j] = S[j], S[i] out[k] ^= S[(S[i] + S[j]) & 0xFF]
print(out.decode())最终输出flag为:ISCTF{Y0u_a2e_2ea11y_a_1aby2inth_master}
小蓝鲨的单片机_1
以前没接触过硬件反编译,这次出了两道硬件逆向的题,只好硬着头皮做了。
首先打开压缩包发现这些东东(我不认识啊)。

之后直接使用8051反汇编器进行反汇编:
--- Pass 1: Analyzing code flow... ------ Pass 2: Generating disassembly (31 code bytes, 4 labels identified) ---
0000: 21 00 AJMP L_0100L_0100:0100: 75 80 FF MOV P0, #0FFH0103: 75 A0 FF MOV P2, #0FFH0106: 90 02 07 MOV DPTR, #00207HL_0109:0109: E5 A0 MOV A, P2010B: F4 CPL A010C: F5 A0 MOV P2, A010E: 74 00 MOV A, #000H0110: 31 1C ACALL L_011C0112: A3 INC DPTR0113: 93 MOVC A, @A+DPTR0114: F4 CPL A0115: F5 80 MOV P0, A0117: B4 00 EF CJNE A, #000H, L_0109011A: 80 E4 SJMP L_0100L_011C:011C: 00 NOP011D: 00 NOP011E: 00 NOP011F: C0 30 PUSH 30H0121: C0 31 PUSH 31H0123: C0 32 PUSH 32H0125: 75 30 22 MOV 30H, #022H0128: 75 31 9F MOV 31H, #09FH012B: 75 32 38 MOV 32H, #038HL_012E:012E: D5 32 FD DJNZ 32H, L_012E0131: D5 31 FA DJNZ 31H, L_012E0134: D5 30 F7 DJNZ 30H, L_012E0137: D0 32 POP 32H0139: D0 31 POP 31H012B: D0 30 POP 30H012D: 22 RET0208: DB 03CH, 018H, 018H, 018H, 018H, 018H, 03CH, 000H0210: DB 03CH, 042H, 040H, 03CH, 002H, 042H, 03CH, 000H... (省略部分数据)扔给ai翻译之后发现数据区很像 8×8 点阵字模,尤其前面几组非常像常见 8×8 大写字母:
- 0208 这一组看起来像 I
- 0210 很像 S
- 0218 很像 C
- 0220 很像 T
- 0228 很像 F
所以把剩下的全部弄出来就好了。
以下是代码:
from PIL import Image, ImageDraw
# 末尾 0xFF 是结束符,不属于 8x8 字符本体ROM_BYTES = [ 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, # 0208 0x3C, 0x42, 0x40, 0x3C, 0x02, 0x42, 0x3C, 0x00, # 0210 0x3C, 0x42, 0x40, 0x40, 0x40, 0x42, 0x3C, 0x00, # 0218 0x7E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, # 0220 0x7E, 0x40, 0x40, 0x7C, 0x40, 0x40, 0x40, 0x00, # 0228 # ... 省略部分数据 0xFF, # 02F0 terminator]
def chunk_glyphs(data): # 移除末尾 terminator 0xFF(只移除尾部连续的 FF) while data and data[-1] == 0xFF: data = data[:-1] # 8 字节一组 if len(data) % 8 != 0: raise ValueError(f"Byte length not multiple of 8: {len(data)}") return [data[i:i+8] for i in range(0, len(data), 8)]
def glyph_to_matrix(glyph, msb_left=True): # 每个字节一行 m = [] for b in glyph: row = [] for x in range(8): bit = (b >> (7 - x)) & 1 if msb_left else (b >> x) & 1 row.append(bit) m.append(row) return m
def transpose8(m): return [[m[y][x] for y in range(8)] for x in range(8)]
def render_strip(glyphs, scale=16, gap=1, msb_left=True, invert=False, by_column=False): """ scale: 每个点放大倍数 gap: 字符间间隔(以"原始像素"为单位,会随 scale 放大) msb_left: True=bit7在左 invert: True=黑白反转 by_column: True=把字模按"列存储"理解(相当于转置) """ mats = [] for g in glyphs: m = glyph_to_matrix(g, msb_left=msb_left) if by_column: m = transpose8(m) if invert: m = [[1 - v for v in row] for row in m] mats.append(m)
n = len(mats) width = n * 8 * scale + (n - 1) * gap * scale height = 8 * scale
img = Image.new("1", (width, height), 1) # 1=白底 draw = ImageDraw.Draw(img)
x_offset = 0 for m in mats: for y in range(8): for x in range(8): if m[y][x]: x0 = x_offset + x * scale y0 = y * scale draw.rectangle([x0, y0, x0 + scale - 1, y0 + scale - 1], fill=0) x_offset += 8 * scale + gap * scale
return img
if __name__ == "__main__": glyphs = chunk_glyphs(ROM_BYTES)最后渲染出来就是flag。
![]()
小蓝鲨的单片机_2
附件还是一堆乱七八糟的和一个note.txt。
其中note.txt的内容是:
;EN = P2.7;RS = P2.6 ;PUZHONG-A2;RW = P2.5
;EN = P2.1;RS = P2.4 ;IKMSIK V2.1;RW = P2.3
;DATA = P0;we use R0 register as the param register.;we use R1 register as the Line-Column register.;we use R3,R4 registers as the str_addr register, R3 = High_addr, R4 = Low_addr.;Databuffer is P0 SFR Register.;R7 register is the counter扔给ai后发现是一段给 8051 + 1602/HD44780 LCD 驱动程序用的”硬件连线说明 + 寄存器约定”注释。
作用就是:
- 告诉你 LCD 怎么接线
- 告诉你这些汇编函数怎么”传参”
但其实你把汇编码直接扔给ai也是一样的。
因此通过51反汇编之后发现汇编码主要做了这些工作:
- 配置端口
- 通过 P0(数据) + P2(控制) 初始化并驱动 1602/HD44780 LCD
- 使用忙标志轮询 + 软件延时保证时序
- 从程序存储器中取两行”被 XOR 0xA2 混淆过的字符串”
- 解码后显示在 LCD 的第一、第二行
- 最终进入死循环保持显示
下面是关键数据:
01CC: EB F1 E1 F6 E4 D9 F5 CD D5 FD FB CD D7 FD E3 D001DC: C7 FD 97 93 FD EF C3 D1 D6 C7 D0 DF 82 82 82 82按程序逻辑每个字节 XOR 0xA2 后就是要显示的 ASCII。
最终解出:ISCTF{Wow_You_Are_51_Master}
VM_COOL
重量级来了,vm壳真是一个硬骨头。
之前N1CTF的时候就遇到VM壳了,当时真是一点不会,现在特意弄了一下vm壳是怎么个原理。
简单来说就是程序运行时通过解释操作码(opcode)选择对应的函数(handle)执行。
那么啥也不说了直接拖到ida里面看一下主函数吧:
int __fastcall main(int argc, const char **argv, const char **envp){ __int64 v4[4]; // [rsp+0h] [rbp-220h] BYREF __int64 v5; // [rsp+20h] [rbp-200h] __int64 v6; // [rsp+28h] [rbp-1F8h] __int64 v7; // [rsp+30h] [rbp-1F0h] _BYTE v8[15]; // [rsp+38h] [rbp-1E8h] char v9; // [rsp+47h] [rbp-1D9h] // ... 省略部分变量声明 unsigned int i; // [rsp+21Ch] [rbp-4h]
v9 = 0; v10 = 0LL; // ... 省略初始化代码 v4[0] = vm_program; v4[1] = qword_4068; v4[2] = qword_4070; v4[3] = qword_4078; v5 = qword_4080; v6 = qword_4088; LOWORD(v5) = -21737; BYTE2(v5) = 55; BYTE5(v4[0]) = 1; v7 = encrypted_flag; *(_QWORD *)v8 = qword_4048; *(_QWORD *)&v8[7] = *(__int64 *)((char *)&qword_4048 + 7); init_vm(v33, v4, 256LL); run_vm(v33); printf("Decrypted flag: "); for ( i = 0; i <= 0x16; ++i ) putchar((unsigned __int8)v33[i + 64]); putchar(10); return 0;}这段 main 的工作流程是:
- 准备 VM 所需的指令流/常量段/配置段
- 运行前对其中几个值做字节级修补
- 拷贝一段15 字节关键常量
- init_vm 建立 VM 上下文
- run_vm 执行 VM 解密逻辑
- 从 v33+64 开始取出 23 字节当作 flag 打印

init_vm
_DWORD *__fastcall init_vm(_DWORD *a1, const void *a2, int a3){ _DWORD *result; // rax
memset(a1, 0, 0x110uLL); memcpy(a1, a2, a3); result = a1; a1[67] = 1; return result;}这是VM 上下文初始化函数,把 VM 运行所需的初始数据块(从栈上拼好的那一坨)拷贝进 VM 上下文,并打一个”已就绪”标记。
run_vm
__int64 __fastcall run_vm(__int64 a1){ __int64 result; // rax
while ( 1 ) { result = *(unsigned int *)(a1 + 268); if ( !(_DWORD)result ) break; result = *(unsigned __int16 *)(a1 + 264); if ( (unsigned __int16)result > 0xFFu ) break; execute_instruction(a1); } return result;}run_vm 就是:当”运行标志=1”且”指令指针 IP 在 0..255 范围内”时,循环取指并执行。
发现有一个execute_instruction函数,我们也看一下(代码较长,这里省略具体实现)。
发现这是一个VM 指令解释器,execute_instruction 包含 opcode 的读取/解码逻辑 + dispatch + handlers。
之后就是疯狂看代码,对该程序进行分析,导致我这道题做了两天。
VM内存布局
┌─────────────────────────────────────────────┐│ 偏移 0x000-0x0FF (256字节) ││ 代码/数据混合区域 ││ - 字节码程序 ││ - 加密的flag数据 ││ - 临时存储 │├─────────────────────────────────────────────┤│ 偏移 0x100-0x107 (8字节) ││ 寄存器区域 (R0-R7) ││ - 8个8位通用寄存器 │├─────────────────────────────────────────────┤│ 偏移 0x108-0x109 (2字节) ││ PC 程序计数器 (16位) │├─────────────────────────────────────────────┤│ 偏移 0x10A (1字节) ││ FLAG 标志寄存器 │├─────────────────────────────────────────────┤│ 偏移 0x10C-0x10F (4字节) ││ 运行状态标志 (1=运行, 0=停止) │└─────────────────────────────────────────────┘初始化流程
- init_vm: 清零内存,复制字节码,设置运行标志为1
- run_vm: 循环执行指令直到运行标志为0或PC超过255
- execute_instruction: 解码并执行单条指令
Opcode表
| Opcode | 助记符 | 参数 | 功能 | IDA地址 |
|---|---|---|---|---|
| 0x01 | LOAD | reg, [addr] | 从内存加载到寄存器 | 0x1241 |
| 0x02 | STORE | [addr], reg | 从寄存器存储到内存 | 0x12B8 |
| 0x03 | ADD | rd, rs1, rs2 | 寄存器加法 | 0x132F |
| 0x04 | SUB | rd, rs1, rs2 | 寄存器减法 | 0x13E6 |
| 0x05 | XOR | rd, rs1, rs2 | 寄存器异或 | 0x149D |
| 0x06 | SHL | rd, rs | 左移 | 0x1554 |
| 0x07 | SHR | rd, rs | 右移 | 0x15EE |
| 0x08 | JMP | addr | 无条件跳转 | 0x1688 |
| 0x09 | JZ | addr, reg | 零跳转 | 0x16C5 |
| 0x0A | CMP | r1, r2 | 比较 | 0x1745 |
| 0xFF | HALT | - | 停机 | 0x17CA |
| 其他 | ERROR | - | 错误 | 0x17DD |
到现在,我们应该已经把该VM的逻辑摸的差不多了,但是其中有许多坑点。
混淆与陷阱
- 偏移 0x06 的 opcode=0x10 未定义,执行两条 LOAD 后触发 “Unknown opcode”,VM 立即停机。
- main将字节码 0x20-0x22 改写为
17 AB 37(同时把字节 0x05 置 0x01),破坏了原本的 STORE+ADD+CMP+JZ+JMP 解密循环。
所以说这是永远无法到达的解密的彼岸。

那么我们应该怎么做呢?
无需跑原 VM,只要复原循环或直接重写算法即可。
对的,我们只需要使用opcode那套鸟语来把算法翻译出来并解密就好了。
最终还原
解密算法(VM 循环核心 基于XOR和加法的混合变换)
真实变换(加密方向):r4 = (((x ^ key1) + key2) ^ key1) - key2
main 写入的密钥:key1 = 0xAB,key2 = 0x37;字节 0x20 的 0x17 表示长度。
逆变换(解密):
t3 = (c + 0x37) & 0xFFt2 = t3 ^ 0xABt1 = (t2 - 0x37) & 0xFFp = t1 ^ 0xAB对 encrypted_flag 的每个字节执行上述逆变换即可。
解出的明文:flag{VM_1s_reALly_c0oL}
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时









