REVERSE-Conway’s trap

ISCC2026 WriteUp 提交模板

REVERSE-Conway’s trap

解题思路

1.先简单看下交互:

image.png

程序会先读取一个seed,然后判断是否正确,
打开IDA看一下,正确后走什么逻辑:

image.png

就在main函数中,seed输入正确的话,就会要求输入flag了。
我们去分析一下seed的逻辑:

image.png

在seed附近,有这么一段逻辑:

  cur = v96[0];                                 // 指向“原始 seed 字节数组”的当前位置
  n32_1 = 0;
  delta = (char *)v94[0] - (char *)v96[0];
  do
  {
    v11 = (void **)seed_str[0];
    v12 = (void **)seed_str[0];
    if ( n15 < 16 )                             // 循环 16 次,把 32 个 hex 字符变成 16 个字节
      v12 = seed_str;
    v13 = *((_BYTE *)v12 + n32_1);
    if ( n15 < 0x10 )
      v11 = seed_str;
    v14 = *((_BYTE *)v11 + n32_1 + 1);
    if ( (unsigned __int8)(v13 - 48) > 9u )
    {
      if ( (unsigned __int8)(v13 - 97) > 5u )
      {
        if ( (unsigned __int8)(v13 - 65) > 5u )
          v15 = 0;
        else
          v15 = v13 - 55;
      }
      else
      {
        v15 = v13 - 87;
      }
    }
    else
    {
      v15 = v13 - 48;
    }
    if ( (unsigned __int8)(v14 - 48) > 9u )
    {
      if ( (unsigned __int8)(v14 - 97) > 5u )
      {
        if ( (unsigned __int8)(v14 - 65) > 5u )
          v16 = 0;
        else
          v16 = v14 - 55;
      }
      else
      {
        v16 = v14 - 87;
      }
    }
    else
    {
      v16 = v14 - 48;
    }
    v17 = v16 | (16 * v15);                     // 组合成一个字节

image.png

seed 输入长度要求为 0x20,随后每两个十六进制字符转成 1 字节,共得到 16 字节。之后程序对这 16 字节调用 0x401990 五次,并把结果按 %02x 格式转回 32 字节 hex 字符串,与运行时构造的目标串比较。

在比较点 0x4022cc 读取全局 std::string,目标串为:

df7b6a5d4da0f5facf32c4ee4b28b792

因此可以反向实现 0x401990,从目标状态倒推原始 seed。

这个字符串是从交叉引用后在这里找到,是通过CRT去初始化的:

image.png

2.逆向seed

image.png

0x401990 每轮先按偶奇字节混合:

new_even = old_even + old_odd
new_odd  = rol8(new_even ^ old_odd, 1)

整体左移 1 字节,把原第 0 字节放到末尾。因为两个操作都是可逆的,所以从目标状态逆 5 轮即可得到 seed:

0b5e321c68e0e4fb2d972226e8c70f8d

3.flag逻辑

继续走,就到了flag的逻辑了

image.png

image.png

这个函数会提取flag的中间部分

image.png

然后sub_401EB0 里比较生成出来的十六进制串和目标串。

在 sub_401EB0 里调用了 sub_401E00。sub_401E00 ,上来就在算两个字符串的 hash,然后分别去解析:

  • 0x401E02:"ntdll.dll"
  • 0x401E39:"RtlAddVectoredExceptionHandler"

这已经很像“运行时解析 API”,而且解析的偏偏是 AddVectoredExceptionHandler。
接着看 0x401E70 之后的调用:

401e70 push offset sub_401DC0 
401e75 push 1 401e77 call eax

eax 就是刚解析出来的 RtlAddVectoredExceptionHandler。参数形状也完全对得上:
RtlAddVectoredExceptionHandler(1, sub_401DC0);
到这一步,VEH 已经基本坐实了。
然后继续往下看,发现它不是只注册 handler 就完事,而是马上改代码:

401e84  mov     eax, offset sub_401CB0
401e89  mov     dl, [eax]
401e8b  mov     byte_41E46A, dl
401e91  mov     byte ptr [eax], 0CCh
401e94  mov     byte_41E46B, 1
  • 保存 sub_401CB0 的首字节
  • 把 sub_401CB0 首字节改成 0xCC

0xCC 就是 int3。看到“注册 VEH + 写 0xCC 到代码段”这一组组合拳,基本就已经可以判定这是用 breakpoint 异常做控制流跳转了

最后再看 handler sub_401DC0

401dc8  cmp     dword ptr [eax], 80000003h
401dd0  cmp     dword ptr [eax+0Ch], offset sub_401CB0
401ddc  mov     dword ptr [eax+0B8h], offset sub_401D00
401de6  or      eax, 0FFFFFFFFh

所以真正的 flag 校验在 0x401d00。该函数先把栈上的 23 字节常量逐字节异或 0xcc,得到期望数组:

image.png

80 de 09 09 a6 a8 f2 30 32 d1 c3 88 4f c4 ef b8 5c 17 07 fb 05 6e 3e

然后对 inner[i] 做如下比较:

rol8(((seed[i & 15] ^ inner[i]) + i * 23) & 0xff, 3) ^ 0xaa == expected[i]

已知 seed 和 expected,我们就可以逐字节逆出 inner[i]。

image.png

Exp

#!/usr/bin/env python3
from struct import pack

TARGET = bytes.fromhex("df7b6a5d4da0f5facf32c4ee4b28b792")


def rol(x, n):
    return ((x << n) & 0xff) | (x >> (8 - n))


def ror(x, n):
    return (x >> n) | ((x << (8 - n)) & 0xff)


def undo_round(state):
    # 先还原最后的整体左移一字节
    mixed = [state[-1], *state[:-1]]

    # 再逐组还原偶奇字节混合
    plain = [0] * 16
    for i in range(0, 16, 2):
        new_a, new_b = mixed[i], mixed[i + 1]
        old_b = ror(new_b, 1) ^ new_a
        old_a = (new_a - old_b) & 0xff
        plain[i] = old_a
        plain[i + 1] = old_b

    return bytes(plain)


def get_seed():
    state = TARGET
    for _ in range(5):
        state = undo_round(state)
    return state


def get_flag(seed):
    enc = (
        pack("<I", 0xc5c5124c)
        + pack("<I", 0xfc3e646a)
        + pack("<I", 0x440f1dfe)
        + pack("<I", 0x74230883)
        + pack("<I", 0x37cbdb90)
        + pack("<H", 0xa2c9)
        + bytes([0xf2])
    )

    target = [x ^ 0xcc for x in enc]
    inner = []

    for i, want in enumerate(target):
        t = ror(want ^ 0xaa, 3)
        ch = ((t - i * 23) & 0xff) ^ seed[i % 16]
        inner.append(ch)

    return b"ISCC{" + bytes(inner) + b"}"


def get_success_text():
    key = b"DoNotPatchMe"
    data = [
        0x08200007, 0x01153106, 0x0c39090f, 0x4e3d012b,
        0x010e0954, 0x132c0043, 0x00284f21, 0x54053e01,
        0x45280017, 0x0a3b1d30, 0x150d3654,
    ]

    cipher = b"".join(pack("<I", x) for x in data) + pack("<H", 0x4604)
    return bytes(c ^ key[i % len(key)] for i, c in enumerate(cipher))


seed = get_seed()
flag = get_flag(seed)

print(f"seed: {seed.hex()}")
print(f"flag: {flag.decode()}")
print(f"message: {get_success_text().decode()}")

posted @ 2026-05-19 16:28  MillionMind  阅读(19)  评论(0)    收藏  举报