ISCTF逆向题回顾2025.12.8

chaoji_xinren 发布于 9 天前 36 次阅读


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!"

#### XOR
tmp = bytes(b ^ xor_key[i % len(xor_key)] for i, b in enumerate(enc))

#### RC4 变体
S = list(range(256)); j = 0
for 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 = 0
out = 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_0100

L_0100:
0100:   75 80 FF     MOV P0, #0FFH
0103:   75 A0 FF     MOV P2, #0FFH
0106:   90 02 07     MOV DPTR, #00207H

L_0109:
0109:   E5 A0        MOV A, P2
010B:   F4           CPL A
010C:   F5 A0        MOV P2, A
010E:   74 00        MOV A, #000H
0110:   31 1C        ACALL L_011C
0112:   A3           INC DPTR
0113:   93           MOVC A, @A+DPTR
0114:   F4           CPL A
0115:   F5 80        MOV P0, A
0117:   B4 00 EF     CJNE A, #000H, L_0109
011A:   80 E4        SJMP L_0100

L_011C:
011C:   00           NOP
011D:   00           NOP
011E:   00           NOP
011F:   C0 30        PUSH 30H
0121:   C0 31        PUSH 31H
0123:   C0 32        PUSH 32H
0125:   75 30 22     MOV 30H, #022H
0128:   75 31 9F     MOV 31H, #09FH
012B:   75 32 38     MOV 32H, #038H

L_012E:
012E:   D5 32 FD     DJNZ 32H, L_012E
0131:   D5 31 FA     DJNZ 31H, L_012E
0134:   D5 30 F7     DJNZ 30H, L_012E
0137:   D0 32        POP 32H
0139:   D0 31        POP 31H
013B:   D0 30        POP 30H
013D:   22           RET
0208:                DB      03CH, 018H, 018H, 018H, 018H, 018H, 03CH, 000H
0210:                DB      03CH, 042H, 040H, 03CH, 002H, 042H, 03CH, 000H
0218:                DB      03CH, 042H, 040H, 040H, 040H, 042H, 03CH, 000H
0220:                DB      07EH, 008H, 008H, 008H, 008H, 008H, 008H, 000H
0228:                DB      07EH, 040H, 040H, 07CH, 040H, 040H, 040H, 000H
0230:                DB      01EH, 010H, 010H, 020H, 010H, 010H, 01EH, 000H
0238:                DB      042H, 042H, 042H, 05AH, 07EH, 066H, 042H, 000H
0240:                DB      000H, 000H, 03CH, 042H, 042H, 042H, 03CH, 000H
0248:                DB      000H, 000H, 042H, 042H, 05AH, 07EH, 042H, 000H
0250:                DB      000H, 000H, 000H, 000H, 000H, 000H, 07EH, 000H
0258:                DB      042H, 042H, 042H, 03CH, 018H, 018H, 018H, 000H
0260:                DB      000H, 000H, 03CH, 042H, 042H, 042H, 03CH, 000H
0268:                DB      000H, 000H, 042H, 042H, 042H, 042H, 03EH, 000H
0270:                DB      000H, 000H, 000H, 000H, 000H, 000H, 07EH, 000H
0278:                DB      018H, 024H, 042H, 042H, 07EH, 042H, 042H, 000H
0280:                DB      000H, 000H, 05CH, 062H, 040H, 040H, 040H, 000H
0288:                DB      000H, 000H, 03CH, 042H, 07EH, 040H, 03CH, 000H
0290:                DB      000H, 000H, 000H, 000H, 000H, 000H, 07EH, 000H
0298:                DB      03CH, 042H, 040H, 04EH, 042H, 042H, 03EH, 000H
02A0:                DB      000H, 000H, 03CH, 042H, 042H, 042H, 03CH, 000H
02A8:                DB      000H, 000H, 03CH, 042H, 042H, 042H, 03CH, 000H
02B0:                DB      002H, 002H, 032H, 04AH, 04AH, 04AH, 03EH, 000H
02B8:                DB      000H, 000H, 000H, 000H, 000H, 000H, 07EH, 000H
02C0:                DB      018H, 024H, 042H, 042H, 07EH, 042H, 042H, 000H
02C8:                DB      010H, 010H, 07EH, 010H, 010H, 010H, 00EH, 000H
02D0:                DB      000H, 000H, 000H, 000H, 000H, 000H, 07EH, 000H
02D8:                DB      03EH, 020H, 020H, 03CH, 002H, 002H, 03CH, 000H
02E0:                DB      010H, 030H, 010H, 010H, 010H, 010H, 038H, 000H
02E8:                DB      078H, 008H, 008H, 00CH, 008H, 008H, 078H, 000H
02F0:                DB      0FFH

--- 反汇编完成 ---

扔给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
    0x1E, 0x10, 0x10, 0x20, 0x10, 0x10, 0x1E, 0x00,  # 0230
    0x42, 0x42, 0x42, 0x5A, 0x7E, 0x66, 0x42, 0x00,  # 0238
    0x00, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x3C, 0x00,  # 0240
    0x00, 0x00, 0x42, 0x42, 0x5A, 0x7E, 0x42, 0x00,  # 0248
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00,  # 0250
    0x42, 0x42, 0x42, 0x3C, 0x18, 0x18, 0x18, 0x00,  # 0258
    0x00, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x3C, 0x00,  # 0260
    0x00, 0x00, 0x42, 0x42, 0x42, 0x42, 0x3E, 0x00,  # 0268
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00,  # 0270
    0x18, 0x24, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x00,  # 0278
    0x00, 0x00, 0x5C, 0x62, 0x40, 0x40, 0x40, 0x00,  # 0280
    0x00, 0x00, 0x3C, 0x42, 0x7E, 0x40, 0x3C, 0x00,  # 0288
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00,  # 0290
    0x3C, 0x42, 0x40, 0x4E, 0x42, 0x42, 0x3E, 0x00,  # 0298
    0x00, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x3C, 0x00,  # 02A0
    0x00, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x3C, 0x00,  # 02A8
    0x02, 0x02, 0x32, 0x4A, 0x4A, 0x4A, 0x3E, 0x00,  # 02B0
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00,  # 02B8
    0x18, 0x24, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x00,  # 02C0
    0x10, 0x10, 0x7E, 0x10, 0x10, 0x10, 0x0E, 0x00,  # 02C8
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00,  # 02D0
    0x3E, 0x20, 0x20, 0x3C, 0x02, 0x02, 0x3C, 0x00,  # 02D8
    0x10, 0x30, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00,  # 02E0
    0x78, 0x08, 0x08, 0x0C, 0x08, 0x08, 0x78, 0x00,  # 02E8

    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)

最后是这个

小蓝鲨的单片机_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 D0
01DC: 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]
  __int64 v10; // [rsp+48h] [rbp-1D8h]
  __int64 v11; // [rsp+50h] [rbp-1D0h]
  __int64 v12; // [rsp+58h] [rbp-1C8h]
  __int64 v13; // [rsp+60h] [rbp-1C0h]
  __int64 v14; // [rsp+68h] [rbp-1B8h]
  __int64 v15; // [rsp+70h] [rbp-1B0h]
  __int64 v16; // [rsp+78h] [rbp-1A8h]
  __int64 v17; // [rsp+80h] [rbp-1A0h]
  __int64 v18; // [rsp+88h] [rbp-198h]
  __int64 v19; // [rsp+90h] [rbp-190h]
  __int64 v20; // [rsp+98h] [rbp-188h]
  __int64 v21; // [rsp+A0h] [rbp-180h]
  __int64 v22; // [rsp+A8h] [rbp-178h]
  __int64 v23; // [rsp+B0h] [rbp-170h]
  __int64 v24; // [rsp+B8h] [rbp-168h]
  __int64 v25; // [rsp+C0h] [rbp-160h]
  __int64 v26; // [rsp+C8h] [rbp-158h]
  __int64 v27; // [rsp+D0h] [rbp-150h]
  __int64 v28; // [rsp+D8h] [rbp-148h]
  __int64 v29; // [rsp+E0h] [rbp-140h]
  __int64 v30; // [rsp+E8h] [rbp-138h]
  __int64 v31; // [rsp+F0h] [rbp-130h]
  __int64 v32; // [rsp+F8h] [rbp-128h]
  char v33[284]; // [rsp+100h] [rbp-120h] BYREF
  unsigned int i; // [rsp+21Ch] [rbp-4h]

  v9 = 0;
  v10 = 0LL;
  v11 = 0LL;
  v12 = 0LL;
  v13 = 0LL;
  v14 = 0LL;
  v15 = 0LL;
  v16 = 0LL;
  v17 = 0LL;
  v18 = 0LL;
  v19 = 0LL;
  v20 = 0LL;
  v21 = 0LL;
  v22 = 0LL;
  v23 = 0LL;
  v24 = 0LL;
  v25 = 0LL;
  v26 = 0LL;
  v27 = 0LL;
  v28 = 0LL;
  v29 = 0LL;
  v30 = 0LL;
  v31 = 0LL;
  v32 = 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 的工作流程是:

  1. 准备 VM 所需的指令流/常量段/配置段
  2. 运行前对其中几个值做字节级修补
  3. 拷贝一段15 字节关键常量
  4. init_vm 建立 VM 上下文
  5. run_vm 执行 VM 解密逻辑
  6. 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函数,我们也看一下

__int64 __fastcall execute_instruction(__int64 a1)
{
  unsigned __int16 v1; // ax
  unsigned __int16 v2; // ax
  unsigned __int16 v3; // ax
  char v4; // cl
  __int64 result; // rax
  unsigned __int16 v6; // ax
  unsigned __int16 v7; // ax
  char v8; // cl
  unsigned __int16 v9; // ax
  unsigned __int16 v10; // ax
  unsigned __int16 v11; // ax
  char v12; // cl
  unsigned __int16 v13; // ax
  unsigned __int16 v14; // ax
  unsigned __int16 v15; // ax
  char v16; // cl
  unsigned __int16 v17; // ax
  unsigned __int16 v18; // ax
  unsigned __int16 v19; // ax
  unsigned __int16 v20; // ax
  unsigned __int16 v21; // ax
  int v22; // esi
  unsigned __int16 v23; // ax
  unsigned __int16 v24; // ax
  int v25; // esi
  unsigned __int16 v26; // ax
  __int16 v27; // dx
  unsigned __int16 v28; // ax
  unsigned __int16 v29; // ax
  unsigned __int16 v30; // ax
  unsigned __int16 v31; // ax
  bool v32; // dl
  unsigned __int8 v33; // [rsp+1Ch] [rbp-4h]
  unsigned __int8 v34; // [rsp+1Dh] [rbp-3h]
  unsigned __int8 v35; // [rsp+1Dh] [rbp-3h]
  unsigned __int8 v36; // [rsp+1Dh] [rbp-3h]
  unsigned __int8 v37; // [rsp+1Eh] [rbp-2h]
  unsigned __int8 v38; // [rsp+1Eh] [rbp-2h]
  unsigned __int8 v39; // [rsp+1Eh] [rbp-2h]
  unsigned __int8 v40; // [rsp+1Eh] [rbp-2h]
  unsigned __int8 v41; // [rsp+1Eh] [rbp-2h]
  unsigned __int8 v42; // [rsp+1Eh] [rbp-2h]
  unsigned __int8 v43; // [rsp+1Eh] [rbp-2h]
  unsigned __int8 v44; // [rsp+1Eh] [rbp-2h]
  unsigned __int8 v45; // [rsp+1Eh] [rbp-2h]
  unsigned __int8 v46; // [rsp+1Fh] [rbp-1h]

  v1 = *(_WORD *)(a1 + 264);
  *(_WORD *)(a1 + 264) = v1 + 1;
  v46 = *(_BYTE *)(a1 + v1);
  if ( v46 > 0xAu )
  {
    if ( v46 == 255 )
    {
      result = a1;
      *(_DWORD *)(a1 + 268) = 0;
      return result;
    }
    goto LABEL_18;
  }
  if ( !v46 )
  {
LABEL_18:
    printf("Unknown opcode: 0x%02X\n", v46);
    result = a1;
    *(_DWORD *)(a1 + 268) = 0;
    return result;
  }
  switch ( v46 )
  {
    case 1u:
      v2 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v2 + 1;
      v37 = *(_BYTE *)(a1 + v2);
      v3 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v3 + 1;
      v4 = *(_BYTE *)(a1 + *(unsigned __int8 *)(a1 + v3));
      result = v37;
      *(_BYTE *)(a1 + v37 + 256) = v4;
      break;
    case 2u:
      v6 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v6 + 1;
      v38 = *(_BYTE *)(a1 + v6);
      v7 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v7 + 1;
      v8 = *(_BYTE *)(a1 + *(unsigned __int8 *)(a1 + v7) + 256);
      result = v38;
      *(_BYTE *)(a1 + v38) = v8;
      break;
    case 3u:
      v9 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v9 + 1;
      v39 = *(_BYTE *)(a1 + v9);
      v10 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v10 + 1;
      v34 = *(_BYTE *)(a1 + v10);
      v11 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v11 + 1;
      v12 = *(_BYTE *)(a1 + *(unsigned __int8 *)(a1 + v11) + 256) + *(_BYTE *)(a1 + v34 + 256);
      result = v39;
      *(_BYTE *)(a1 + v39 + 256) = v12;
      break;
    case 4u:
      v13 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v13 + 1;
      v40 = *(_BYTE *)(a1 + v13);
      v14 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v14 + 1;
      v35 = *(_BYTE *)(a1 + v14);
      v15 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v15 + 1;
      v16 = *(_BYTE *)(a1 + v35 + 256) - *(_BYTE *)(a1 + *(unsigned __int8 *)(a1 + v15) + 256);
      result = v40;
      *(_BYTE *)(a1 + v40 + 256) = v16;
      break;
    case 5u:
      v17 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v17 + 1;
      v41 = *(_BYTE *)(a1 + v17);
      v18 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v18 + 1;
      v36 = *(_BYTE *)(a1 + v18);
      v19 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v19 + 1;
      v33 = *(_BYTE *)(a1 + v19);
      result = v41;
      *(_BYTE *)(a1 + v41 + 256) = *(_BYTE *)(a1 + v33 + 256) ^ *(_BYTE *)(a1 + v36 + 256);
      break;
    case 6u:
      v20 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v20 + 1;
      v42 = *(_BYTE *)(a1 + v20);
      v21 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v21 + 1;
      v22 = *(unsigned __int8 *)(a1 + v42 + 256) << *(_BYTE *)(a1 + *(unsigned __int8 *)(a1 + v21) + 256);
      result = v42;
      *(_BYTE *)(a1 + v42 + 256) = v22;
      break;
    case 7u:
      v23 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v23 + 1;
      v43 = *(_BYTE *)(a1 + v23);
      v24 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v24 + 1;
      v25 = (int)*(unsigned __int8 *)(a1 + v43 + 256) >> *(_BYTE *)(a1 + *(unsigned __int8 *)(a1 + v24) + 256);
      result = v43;
      *(_BYTE *)(a1 + v43 + 256) = v25;
      break;
    case 8u:
      v26 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v26 + 1;
      v27 = *(unsigned __int8 *)(a1 + v26);
      result = a1;
      *(_WORD *)(a1 + 264) = v27;
      break;
    case 9u:
      v28 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v28 + 1;
      v44 = *(_BYTE *)(a1 + v28);
      v29 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v29 + 1;
      result = *(unsigned __int8 *)(a1 + *(unsigned __int8 *)(a1 + v29) + 256);
      if ( !(_BYTE)result )
      {
        result = a1;
        *(_WORD *)(a1 + 264) = v44;
      }
      break;
    case 0xAu:
      v30 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v30 + 1;
      v45 = *(_BYTE *)(a1 + v30);
      v31 = *(_WORD *)(a1 + 264);
      *(_WORD *)(a1 + 264) = v31 + 1;
      v32 = *(_BYTE *)(a1 + v45 + 256) == *(_BYTE *)(a1 + *(unsigned __int8 *)(a1 + v31) + 256);
      result = a1;
      *(_BYTE *)(a1 + 266) = v32;
      break;
    default:
      goto LABEL_18;
  }
  return result;
}

发现这是一个VM 指令解释器,execute_instruction 包含 opcode 的读取/解码逻辑 + dispatch + handlers

之后就是疯狂看代码,对该程序进行分析,导致我这道题做了两天

VM内存布局

偏移 0-255 (0x000-0x0FF): 代码/数据区域 (256字节)

偏移 256-263 (0x100-0x107): 寄存器区域 (8个字节寄存器 R0-R7)

偏移 264-265 (0x108-0x109): PC程序计数器 (16位)

偏移 266 (0x10A): 标志寄存器 (8位)

偏移 267 (0x10B): (未使用/填充)

偏移 268-271 (0x10C-0x10F): 运行状态标志 (32位, 1=运行中, 0=停止)
┌─────────────────────────────────────────────┐
│ 偏移 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助记符参数功能IDA地址
0x01LOADreg, [addr]从内存加载到寄存器0x1241
0x02STORE[addr], reg从寄存器存储到内存0x12B8
0x03ADDrd, rs1, rs2寄存器加法0x132F
0x04SUBrd, rs1, rs2寄存器减法0x13E6
0x05XORrd, rs1, rs2寄存器异或0x149D
0x06SHLrd, rs左移0x1554
0x07SHRrd, rs右移0x15EE
0x08JMPaddr无条件跳转0x1688
0x09JZaddr, reg零跳转0x16C5
0x0ACMPr1, r2比较0x1745
0xFFHALT-停机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 = 0xABkey2 = 0x37;字节 0x20 的 0x17 表示长度。

逆变换(解密)

t3 = (c + 0x37) & 0xFF
t2 = t3 ^ 0xAB
t1 = (t2 - 0x37) & 0xFF
p  = t1 ^ 0xAB

对 encrypted_flag 的每个字节执行上述逆变换即可。

  • 解出的明文:flag{VM_1s_reALly_c0oL}

ez_xtea

写太累了歇一会

此作者没有提供个人介绍。
最后更新于 2025-12-08