首页
社区
课程
招聘
[讨论] 南极企鹅游戏安全2026-Android-初赛
发表于: 15小时前 196

[讨论] 南极企鹅游戏安全2026-Android-初赛

15小时前
196

安卓真好玩。想知道有没有去3环程序的思路。

展示:

了解目标、确定方向:

打开游戏,触发示例方块得到样例flag,目标是触发屋顶的方块,得到真正的flag,常规游玩、车是开不到那地方的。

所以第一个目标:传送车的坐标到触发块的坐标。

解包APK发现是Godot引擎制作的游戏,gdc脚本也被加密了,本身对godot不是很熟悉,不过既然是开源的就下载源码下来看看,同时上网搜索发现:Godot官方是支持用脚本加密的。

懒了: 77bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3f1#2x3Y4m8G2K9X3W2W2i4K6u0W2j5$3&6Q4x3V1k6@1K9s2u0W2j5h3c8Q4x3X3b7J5x3o6l9J5y4K6M7&6i4K6u0V1x3g2)9J5k6o6q4Q4x3X3g2Z5N6r3#2D9 ,直接照抄思路dump密钥(脚本末尾)

得到ce4df8753b59a5a39ade58ac07ef947a3da39f2af75e3284d51217c04d49a061 ,

尝试使用社区工具gdre_tools来解密显然是不行的,下载源码后发现脚本解密逻辑中的AES_MODE和游戏逆向出来的处理方式不太一样,要改掉那部分逻辑又得重新下个godot引擎编译很麻烦,干脆直接复用游戏内的解密逻辑,然后再用gdre_tools读取gdc脚本:

游戏引擎(libgodot_android.so)解密魔改部分:

  • sub_3801410这是外层 FileAccessEncrypted::open_and_parse 。
  • 0x380170c检查魔数 1128612935 = 0x43454447 = "GDEC"。
  • 0x38019e4外层真正调用解密 wrapper 的位置。
  • sub_376EF68是一层很薄的 wrapper,直接转到 sub_197DE18。
  • sub_197C210AES key schedule 初始化。
  • sub_197DE18“魔改流模式”核心。

解密脚本:

#!/usr/bin/env python3
from __future__ import annotations

import argparse
import hashlib
import struct
from pathlib import Path


DEFAULT_INPUT = Path("lib/arm64-v8a/libsec2026.so")
DEFAULT_OUTPUT = Path("lib/arm64-v8a/libsec2026.payload.bin")
DEFAULT_MAP_SIZE_OFFSET = 0x69A60
DEFAULT_COMP_SIZE_OFFSET = 0x69A64
DEFAULT_BLOB_OFFSET = 0x69A6C
DEFAULT_ENTRY_OFFSET = 0x10


def parse_int(value: str) -> int:
    return int(value, 0)


def read_u32_le(data: bytes, offset: int) -> int:
    return struct.unpack_from("<I", data, offset)[0]


def to_signed32(value: int) -> int:
    value &= 0xFFFFFFFF
    return value - 0x1_0000_0000 if value & 0x80000000 else value


class Bitstream:
    def __init__(self, data: bytes) -> None:
        self.data = data
        self.offset = 0
        self.word = 0x80000000

    def read_byte(self) -> int:
        if self.offset >= len(self.data):
            raise EOFError("read past compressed blob")
        value = self.data[self.offset]
        self.offset += 1
        return value

    def next_bit(self) -> int:
        carry = 1 if (self.word & 0x80000000) else 0
        self.word = (self.word << 1) & 0xFFFFFFFF
        if self.word == 0:
            if self.offset + 4 > len(self.data):
                raise EOFError("reload past compressed blob")
            reloaded = read_u32_le(self.data, self.offset)
            self.offset += 4
            total = reloaded + reloaded + carry
            carry = 1 if total > 0xFFFFFFFF else 0
            self.word = total & 0xFFFFFFFF
        return carry


def decode_varlen(bitstream: Bitstream) -> int:
    value = 1
    while True:
        value = ((value << 1) + bitstream.next_bit()) & 0xFFFFFFFF
        if bitstream.next_bit():
            return value


def unpack_payload(blob: bytes) -> bytes:
    bitstream = Bitstream(blob)
    out = bytearray()
    last_offset = 0xFFFFFFFF

    while True:
        if bitstream.next_bit():
            out.append(bitstream.read_byte())
            continue

        code = decode_varlen(bitstream)
        if code >= 3:
            offset_low = bitstream.read_byte()
            last_offset = (~(offset_low | ((code - 3) << 8))) & 0xFFFFFFFF
            if last_offset == 0:
                break

        length = bitstream.next_bit()
        length = ((length << 1) + bitstream.next_bit()) & 0xFFFFFFFF
        if length == 0:
            length = (decode_varlen(bitstream) + 2) & 0xFFFFFFFF

        if ((last_offset + 0xD00) >> 32) == 0:
            length = (length + 1) & 0xFFFFFFFF

        src = len(out) + to_signed32(last_offset)
        if src < 0:
            raise ValueError(f"invalid back-reference: src={src} offset={last_offset:#x}")

        copies = length + 1
        for _ in range(copies):
            out.append(out[src])
            src += 1

    if bitstream.offset != len(blob):
        raise ValueError(
            f"decompression ended at 0x{bitstream.offset:x}, expected 0x{len(blob):x}"
        )
    return bytes(out)


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Offline unpacker for the runtime-compressed payload in libsec2026.so."
    )
    parser.add_argument(
        "input",
        nargs="?",
        type=Path,
        default=DEFAULT_INPUT,
        help=f"input shared object (default: {DEFAULT_INPUT})",
    )
    parser.add_argument(
        "-o",
        "--output",
        type=Path,
        default=DEFAULT_OUTPUT,
        help=f"where to write the unpacked payload (default: {DEFAULT_OUTPUT})",
    )
    parser.add_argument(
        "--map-size-offset",
        type=parse_int,
        default=DEFAULT_MAP_SIZE_OFFSET,
        help=f"offset of the mapped-size dword (default: {DEFAULT_MAP_SIZE_OFFSET:#x})",
    )
    parser.add_argument(
        "--comp-size-offset",
        type=parse_int,
        default=DEFAULT_COMP_SIZE_OFFSET,
        help=f"offset of the compressed-size dword (default: {DEFAULT_COMP_SIZE_OFFSET:#x})",
    )
    parser.add_argument(
        "--blob-offset",
        type=parse_int,
        default=DEFAULT_BLOB_OFFSET,
        help=f"offset of the compressed blob (default: {DEFAULT_BLOB_OFFSET:#x})",
    )
    parser.add_argument(
        "--entry-offset",
        type=parse_int,
        default=DEFAULT_ENTRY_OFFSET,
        help=f"runtime branch target inside the unpacked payload (default: {DEFAULT_ENTRY_OFFSET:#x})",
    )
    parser.add_argument(
        "--pad-to-map-size",
        action="store_true",
        help="pad the output file to the loader's mmap size",
    )
    return parser


def main() -> int:
    args = build_parser().parse_args()

    image = args.input.read_bytes()
    map_size = read_u32_le(image, args.map_size_offset)
    comp_size = read_u32_le(image, args.comp_size_offset)
    blob = image[args.blob_offset : args.blob_offset + comp_size]
    if len(blob) != comp_size:
        raise ValueError("compressed blob extends past end of file")

    payload = unpack_payload(blob)
    if len(payload) > map_size:
        raise ValueError(
            f"payload is larger than mapped size: 0x{len(payload):x} > 0x{map_size:x}"
        )

    written = payload.ljust(map_size, b"\x00") if args.pad_to_map_size else payload
    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_bytes(written)

    sha256 = hashlib.sha256(payload).hexdigest()
    print(f"input:        {args.input}")
    print(f"output:       {args.output}")
    print(f"map size:     0x{map_size:x}")
    print(f"blob size:    0x{comp_size:x}")
    print(f"payload size: 0x{len(payload):x}")
    print(f"entry offset: 0x{args.entry_offset:x}")
    print(f"entry file:   {args.output}@0x{args.entry_offset:x}")
    print(f"sha256:       {sha256}")
    if args.pad_to_map_size and len(written) != len(payload):
        print(f"padded size:  0x{len(written):x}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

同时获取到了flag的获取逻辑:

然后就是处理传送车的问题:反过来利用 Godot 自己的对象系统和场景系统,直接让游戏正常执行这段逻辑。

根据Unity开发经验猜测:Godot肯定也有某种find gameObject方法,找了一下 Godot 恰好给了这样做的条件。只要在 Frida 里拿到引擎内部的这些能力:

  • 获取全局 singleton;
  • 构造 VariantStringNameString
  • 在主线程上远程调用对象方法;

就可以像脚本层一样直接操纵场景节点。

Frida 远程调用 Godot 内部方法移车

这部分的实现对应脚本是frida_move_trigger_sec2026.js(太长放末尾了)。

核心思路远程调用引擎提供的接口

  1. libgodot_android.so 固定偏移解析出运行时接口。
  2. 自己构造 Godot Variant 参数。
  3. 通过 Engine.get_main_loop() 拿到 SceneTree
  4. 继续拿到 root,再用 find_child() 找到:
    • car
    • Trigger2
    • Label2
  5. 读取 Trigger2 的全局变换,直接把车和物理 body 移过去。

实在懒得搜坐标手动改,还是原生的办法通用性高一些。

如果只是改可见节点的位置,车不一定真的算“进入触发器”,因为场景里真正参与碰撞的是物理对象。也正因为这样,frida_move_trigger_sec2026.js 不是只改一次 transform 就结束了,而是同时做了两层同步:

  1. 对节点本身调用:
    • set_global_transform
    • force_update_transform
  2. PhysicsServer3D 调用:
    • body_set_state(transform)
    • body_set_state(linear_velocity, Vector3.ZERO)
    • body_set_state(angular_velocity, Vector3.ZERO)
    • body_set_state(sleeping, false)

libsec.so分析:

IDA打开可以分析的函数不多:

主要是解压子程序,然后通过BR命令跳转到子程序去执行逻辑。

二环主要逻辑:

00001450        // 二环装载器:根据 arg1
00001450        // 的自相对头字段计算重定位基址与下一环入口;把
00001450        // rebase + *(u32 *)(arg1 + 0xc) 处的内嵌 UPX
00001450        // 风格容器复制到临时映射并解包;遍历 arg3 +
00001450        // 0x40 开始的 0x38
00001450        // 字节描述符,逐段映射、修补并设置权限;如遇可执行尾段则通过
00001450        // memfd 构造 16 字节 RX 跳板;最后在 0x1778
00001450        // 跳入重定位后的下一环入口。
00001458        int64_t x19
00001458        int64_t var_e0 = x19
0000147c        float128 v15
0000147c        sub_1004(arg1, x19, v15)  // 设置描述符号
00001484        // 计算重定位基址:arg1
00001484        // 头字段是自相对偏移,因此 base = arg1 - *(u32
00001484        // *)arg1。
0000148c        double v9 = float.d(arg1 - zx.q(*arg1))
00001490        int32_t x1 = (*(arg1 + 8)).d
00001494        int64_t var_10 = *arg1
000014a4        // 定位内嵌的下一环压缩块:packed_blob = rebase_base
000014a4        // + *(u32 *)(arg1 + 0xc)。
000014a4        char* x19_2 = v9 i+ zx.q(arg1[3])
000014b4        // 定位重定位后的下一环入口:next_entry =
000014b4        // rebase_base + *(u32 *)(arg1 + 4)。
000014b4        double v10 = float.d(v9 i+ zx.q(arg1[1]))
000014b8        uint64_t x0_6 = zx.q(arg1.d - x19_2.d)
000014dc        // mmap
000014dc        // 申请 RW
000014dc        // 临时映射,用来承接下一环的压缩容器。
000014dc        char* x0_7 = sub_74()
000014f0        // 拷贝缓冲区
000014f0        // 把内嵌压缩块复制到临时映射中。
000014f0        sub_f48(x0_7, x19_2, x0_6)
000014fc        uint64_t var_20 = zx.q(*(x0_7 + 0x18))
00001500        void* var_18 = arg3
0000150c        uint64_t var_30 = zx.q(*(x0_7 + 0x1c)) + 0xc
00001510        void* var_28 = &x0_7[0x18]
0000151c        // 初始化 / 推进 scratch+0x18 处的 UPX
0000151c        // 风格块流解析器。
0000151c        sub_1150(&var_30, &var_20)
00001520        // 执行起始地址
00001520        // 描述符表起点:arg3 + 0x40;每项大小 0x38 字节。
00001520        int64_t* x19_3 = arg3 + 0x40
00001530        // desc_end = desc_table + (*(u16 *)(arg3 + 0x38) *
00001530        // 0x38),准备遍历全部段描述符。
00001530        void* x23_1 = &x19_3[zx.q(*(arg3 + 0x38)) * 7]
00001530        
00001538        if (x19_3 u< x23_1)
0000153c            int32_t x24_1 = 0
00001540            int64_t x21_1 = 0
00001540            
00001750            do  // 修补映射的主逻辑
00001550                // 读取 desc->type_flags;只有 (type_flags &
00001550                // 0x2ffffffff) == 1 的项会按可装载段处理。
00001564                if ((*x19_3 & 0x2ffffffff) == 1)
00001568                    if (x21_1 == 0)
0000156c                        // 首个可装载段会建立统一的
0000156c                        // load_bias:load_bias = rebase_base -
0000156c                        // desc->vaddr。
00001574                        x21_1 = v9 i- x19_3[2]
00001574                    
00001578                    // 计算当前段的逻辑末尾 desc->vaddr +
00001578                    // desc->filesz,并从块流中取出下一个 0xc
00001578                    // 字节块头。
00001580                    int32_t x20_2 = (x19_3[2]).d + (x19_3[4]).d
00001584                    var_30 = 0xc
00001594                    int32_t var_50
00001594                    sub_10f0(&var_30, &var_50, 0xc)
000015a0                    var_28 -= 0xc
000015a8                    int32_t var_4c
000015a8                    var_30 = zx.q(var_4c)
000015ac                    int32_t x0_21 = var_50
000015b0                    uint64_t x1_9 = zx.q(x0_21)
000015b4                    // chunk_len =
000015b4                    // hdr[0];记录本次要回填到当前段的块长度。
000015b4                    var_20 = x1_9
000015c0                    // 计算当前块的落点:dst = load_bias +
000015c0                    // (desc->vaddr + desc->filesz - chunk_len)。
000015c0                    int64_t x1_11 = zx.q(x20_2) - x1_9 + x21_1
000015c0                    
000015dc                    if (x19_3[1] + x19_3[4] u> zx.q(x1) || x24_1 != 0)
000015ec                        int32_t var_64_1
000015ec                        
000015ec                        if ((*(x19_3 + 4) & 1) == 0)
00001600                            // 若 desc->flags.bit0
00001600                            // 未置位,则走匿名 RW
00001600                            // 映射路径(sub_f6c)。
00001604                            sub_f6c(x0_21, x1_11, arg1.d)
00001608                            var_64_1 = 0
000015ec                        else
000015f0                            // 若 desc->flags.bit0 置位,则走
000015f0                            // file-backed /
000015f0                            // 对齐映射路径(sub_1374)。
000015f8                            var_64_1 = sub_1374(x0_21, x1_11, arg1)
000015f8                        
0000160c                        // 保存当前 chunk_len /
0000160c                        // dst,随后推进块流到下一个块。
0000160c                        uint32_t x0_23 = var_20.d
00001620                        sub_1150(&var_30, &var_20)
00001620                        
00001628                        // 只有可执行 / file-backed
00001628                        // 类型的段才会继续进入下面的跳板与最终权限处理逻辑。
0000162c                        if ((*(x19_3 + 4) & 1) != 0)
00001644                            int32_t var_40_1 = 0xd4000001
00001650                            int32_t var_3c_1 = 0xa9417be2
0000165c                            int32_t var_38_1 = 0xa8c207e0
00001668                            int32_t var_34_1 = 0xd61f03c0
00001668                            
0000166c                            // 特殊分支:只处理 masked
0000166c                            // type_flags == 0x100000001
0000166c                            // 的描述符(可执行尾段 /
0000166c                            // 跳板场景)。
0000167c                            if ((*x19_3 & 0x1ffffffff) == 0x100000001)
00001680                                // 计算尾部 slack = desc->memsz -
00001680                                // desc->filesz;若满足页对齐约束,则为尾段构造
00001680                                // RX 跳板。
0000169c                                if ((neg.d(x1_11.d + (x19_3[5]).d - (x19_3[4]).d)
0000169c                                        & not.d(arg1.d)) u> 0xf)
0000169c                                    jump(0x16a0)
0000169c                                
000016dc                                int64_t __saved_fp
000016dc                                // 通过 memfd_create -> write(16-byte
000016dc                                // veneer) -> mmap(PROT_RX)
000016dc                                // 生成临时可执行跳板,结果保存在
000016dc                                // x26。
000016dc                                sub_16e4(x19_3, x1_11, x21_1, 0xffffffffffff, x23_1, x24_1, 
000016dc                                    0xc, arg2, x1, &__saved_fp, v9, v10)
000016e0                                undefined
000016e0                            
0000172c                            // 调用 sub_12c4
0000172c                            // 完成当前段的收尾:设权限、刷
0000172c                            // I-cache,并按需要释放中间映射。
00001740                            sub_12c4(x0_23, x1_11, x19_3, var_64_1, x21_1)
00001740                        
00001744                        x24_1 += 1
000015dc                    else
000015e0                        x24_1 += 1
000015e0                
00001748                // 移动到下一个 0x38
00001748                // 字节描述符,继续装载循环。
00001748                x19_3 = &x19_3[7]
00001750            while (x23_1 u> x19_3)
00001750        
00001764        // 释放之前为压缩容器申请的 scratch 映射。
00001764        sub_1c(x0_7, x0_6)
00001778        // 正式跳入下一环入口:blr next_entry(zx.q(*arg2),
00001778        // *(arg2 + 8), *(arg2 + 0x10))。
00001778        v10(zx.q(*arg2), *(arg2 + 8), *(arg2 + 0x10))
0000179c        return 0

选择在00001764 通过Frida脚本动态dump(脚本见末尾)下来继续分析:

三环:

00001c48    int64_t sub_1c48()

00001c48    // 三环入口的首次自修改例程:用 8
00001c48    // 字节常量循环改写 0xa25d2 开始的 0x21
00001c48    // 字节,先解锁后续跳板/代码片段。
00001c48  ff4300d1   sub     sp, sp, #0x10
00001c4c  cac98bd2   mov     x10, #0x5e4e
00001c50  e9031f2a   mov     w9, wzr  {_start}
00001c54  eae9b4f2   movk    x10, #0xa74f, lsl #0x10
00001c58  1f2003d5   nop     
00001c5c    // 定位三环入口后要被原地改写的 0x21 字节区域。
00001c5c  a84b5050   adr     x8, 0xa25d2
00001c60  eadecdf2   movk    x10, #0x6ef7, lsl #0x20
00001c64  6af4f2f2   movk    x10, #0x97a3, lsl #0x30  {-0x685c910858b0a1b2}
00001c68  ea0700f9   str     x10, [sp, #0x8 {var_8}]  {-0x685c910858b0a1b2}

00001c6c  2a7d4093   sxtw    x10, w9
00001c70  eb230091   add     x11, sp, #0x8 {var_8}
00001c74  2b0940b3   bfxil   x11, x9, #0, #0x3 {var_8}
00001c78  2d010012   and     w13, w9, #0x1
00001c7c  0c696a38   ldrb    w12, [x8, x10]
00001c80  6b014039   ldrb    w11, [x11]
00001c84  6e010c0a   and     w14, w11, w12
00001c88  6b010c2a   orr     w11, w11, w12
00001c8c  ec030e4b   neg     w12, w14
00001c90  ee03292a   mvn     w14, w9
00001c94  29010d0b   add     w9, w9, w13
00001c98  ce791f32   orr     w14, w14, #0xfffffffe
00001c9c  6d010c0a   and     w13, w11, w12
00001ca0  29010e0b   add     w9, w9, w14
00001ca4  6b010c4a   eor     w11, w11, w12
00001ca8  29090011   add     w9, w9, #0x2
00001cac  6b050d0b   add     w11, w11, w13, lsl #0x1
00001cb0  3f850071   cmp     w9, #0x21
00001cb4  0b692a38   strb    w11, [x8, x10]
00001cb8  abfdff54   b.lt    0x1c6c

00001cbc  ff430091   add     sp, sp, #0x10
00001cc0  c0035fd6   ret     

因为三环入口 0x1c48 会先自修改代码,静态分析已经啃不动了,看静态毫无头绪,Hook要打上还得抓时机控制、不然直接崩。想着应该可以用Stalker去追执行流说不定方便点,但由于页权限切换、还有自修改代码等原因、反正我是一个个线程去试了,没扒下来。一番静态压根找不到Process的链路,转向动态,从native层和游戏层之间的沟通入手:

动态分析:

根据解包的配置知道,libsec的在游戏中被调用的入口点是

[configuration]
entry_symbol = "extension_init"

而Godot GDExtension的标准API是:

GDExtensionBool extension_init(
    GDExtensionInterfaceGetProcAddress p_get_proc_address,
    GDExtensionClassLibraryPtr p_library,
    GDExtensionInitialization *r_initialization
);

在Godot游戏引擎要调用拓展的时候第一个传入的函数指针p_get_proc_address类似于引擎接口查询函数可以直接查到Godot 的注册接口地址classdb_register_extension_class_method 在注册拓展函数的时候,会交一个GDExtensionClassMethodInfo:

typedef struct GDExtensionClassMethodInfo {
    void *name;                  // StringName*
    void *method_userdata;       // 用户数据
    void *call_func;             // 标准调用入口
    void *ptrcall_func;          // ptrcall 入口
    uint32_t method_flags;       // 方法标志
    uint32_t has_return_value;   // 是否有返回值
    uint8_t _pad0[0x0c];         // 中间未关心字段
    uint32_t argument_count;     // 参数个数
    uint8_t _pad1[0x10];         // 中间未关心字段
    uint32_t default_argument_count;
    uint8_t _pad2[0x0c];         // 结构尾部
} GDExtensionClassMethodInfo;

call_func注册了拓展函数的入口点,注册时可以拿到三环关键函数的地址:

[sec2026_proc] direct classdb_register_extension_class_method class= method= call=libsec2026.so!0x63de4 ptrcall=libsec2026.so!0x63e3c lr=libsec2026.so!0x60bf0
[sec2026_proc] registered method #1 source=direct:libsec2026.so!0x60bf0 class=<unnamed_class> method=<unnamed_method_1> call=libsec2026.so!0x63de4 ptrcall=libsec2026.so!0x63e3c
[sec2026_proc] <unnamed_class>.<unnamed_method_1>@direct:libsec2026.so!0x60bf0.call_func@0x7237065de4 runtime bytes: ff 03 01 d1 fd 7b 02 a9 f3 1b 00 f9 fd 83 00 91 08 00 40 f9 f3 03 04 aa e4 03 05 aa 09 0d 40 f9
[sec2026_proc] <unnamed_class>.<unnamed_method_1>@direct:libsec2026.so!0x60bf0.call_func@0x7237065de4 runtime disassembly:
[sec2026_proc]   0x7237065de4 sub sp, sp, #0x40
[sec2026_proc]   0x7237065de8 stp x29, x30, [sp, #0x20]
[sec2026_proc]   0x7237065dec str x19, [sp, #0x30]
[sec2026_proc]   0x7237065df0 add x29, sp, #0x20
[sec2026_proc]   0x7237065df4 ldr x8, [x0]
[sec2026_proc]   0x7237065df8 mov x19, x4
[sec2026_proc]   0x7237065dfc mov x4, x5
[sec2026_proc]   0x7237065e00 ldr x9, [x8, #0x18]
[sec2026_proc]   0x7237065e04 add x8, sp, #8
[sec2026_proc]   0x7237065e08 blr x9
[sec2026_proc]     &lt;indirect call via [object Object]&gt;
[sec2026_proc]   0x7237065e0c adrp x8, #0x72370ed000
[sec2026_proc]   0x7237065e10 add x1, sp, #8
[sec2026_proc]   0x7237065e14 mov x0, x19
[sec2026_proc]   0x7237065e18 ldr x8, [x8, #0xf48]
[sec2026_proc]   0x7237065e1c ldr x8, [x8]
[sec2026_proc]   0x7237065e20 blr x8
[sec2026_proc]     &lt;indirect call via [object Object]&gt;
[sec2026_proc]   0x7237065e24 add x0, sp, #8
[sec2026_proc]   0x7237065e28 bl #0x7237068dc0
[sec2026_proc]   0x7237065e2c ldp x29, x30, [sp, #0x20]
[sec2026_proc]   0x7237065e30 ldr x19, [sp, #0x30]
[sec2026_proc]   0x7237065e34 add sp, sp, #0x40
[sec2026_proc]   0x7237065e38 ret

对照dump下来的逻辑:

00063de4    uint64_t __convention("apple-arm64-objc-fast-arc-0") sub_63de4(int64_t arg1, 
00063de4      void* arg2 @ x19, void* arg3 @ fp, int64_t arg4, int64_t arg5, int64_t arg6, int64_t arg7)

00063de4    // GameExtension.Process 的 call_func 入口。第一个参数 x0
00063de4    // 就是 method_userdata;运行时会先取 *(x0)
00063de4    // 作为分发表,再取该表的 +0x18
00063de4    // 项作为真实处理函数并间接调用,所以后续稳定落到
00063de4    // sub_4d78c 不是静态直连,而是 slot 分发结果。
00063de4  f40300aa   mov     x20, x0
00063de8  41fbffd0   adrp    x1, 0xfffffffffffcd000
00063dec  21001e91   add     x1, x1, #0x780  {-0x32880}
00063df0  a0630091   add     x0, fp, #0x18
00063df4  e2031f2a   mov     w2, wzr  {_start}
00063df8  52d9fe97   bl      sub_1a340
00063dfc  424b8a52   mov     w2, #0x525a
00063e00  a1630091   add     x1, fp, #0x18
00063e04  e00314aa   mov     x0, x20
00063e08  a218bd72   movk    w2, #0xe8c5, lsl #0x10  {0xe8c5525a}
00063e0c    // 运行时这里是关键间接跳转。按实际执行到的代码看,call_func
00063e0c    // 先用 x0(method_userdata) 取 *x0 作为分发表,再取 [*x0
00063e0c    // + 0x18] 作为真实处理函数;静态里看到的
00063e0c    // x21,本质上就是这个 slot 0x18 取出的函数指针。
00063e0c  a0023fd6   blr     x21
00063e10  f40300aa   mov     x20, x0
00063e14  a0630091   add     x0, fp, #0x18
00063e18  9c3bff97   bl      sub_32c88
00063e1c  000200f0   adrp    x0, 0xa6000
00063e20  00402c91   add     x0, x0, #0xb10  {0xa6b10}
00063e24  148400f8   str     x20, [x0], #0x8  {0xa6b10}  {0xa6b18}
00063e28  44690094   bl      sub_7e338
00063e2c  e00313aa   mov     x0, x19
00063e30  d2ffff17   b       0x63d78

可以发现自修改改了不少,动态分析就省去了那部分分析逻辑,而因为执行函数的时候是通过method_userdata下发分发的,调用时又加上了userdate做了一层偏移,触发flag获取的逻辑后,可以得到Object.Process在三环内的真正地址:

[21091116AC::com.tencent.ACE.gamesec2026.preliminary ]-> [sec2026_proc] <unnamed_class>.<unnamed_method_1>@direct:libsec2026.so!0x60bf0 first dispatch chain via call_func slot=0x18 table=0x72370e9d90 libsec2026.so!0x4d7a8 -> libsec2026.so!0x4ffd0

结合静态来看,开始套娃:

0004d78c    // call_func
0004d78c    // 落下来的第一层调度器。这里先做一次懒初始化,再从全局
0004d78c    // slot
0004d78c    // 里取出真实处理函数,把参数继续分发到下一层。
0004d78c  fd7bbda9   stp     fp, lr, [sp, #-0x30]! {__saved_fp} {__saved_lr}
0004d790  f50b00f9   str     x21, [sp, #0x10 {var_20}]
0004d794  f44f02a9   stp     x20, x19, [sp, #0x20] {__saved_x20} {__saved_x19}
0004d798  fd030091   mov     fp, sp {__saved_fp}
0004d79c  c8020090   adrp    x8, 0xa5000
0004d7a0  08c11991   add     x8, x8, #0x670  {0xa5670}
0004d7a4  08fddf08   ldarb   w8, [x8]  {0xa5670}
0004d7a8    // 第一层稳定调度入口:从这里开始已经不是 Godot
0004d7a8    // 包装层,而是三环内部自己的分发逻辑。
0004d7a8  48020036   tbz     w8, #0, 0x4d7f0

0004d7ac  a9020090   adrp    x9, 0xa1000
0004d7b0  c8020090   adrp    x8, 0xa5000
0004d7b4  a3630091   add     x3, fp, #0x18 {var_18}
0004d7b8  290d41f9   ldr     x9, [x9, #0x218]  {0xa1218}
0004d7bc  083543f9   ldr     x8, [x8, #0x668]  {0xa5668}
0004d7c0  010840f9   ldr     x1, [x0, #0x10]
0004d7c4  e2031faa   mov     x2, xzr  {_start}
0004d7c8  290140f9   ldr     x9, [x9]
0004d7cc  e00308aa   mov     x0, x8
0004d7d0    // 通过 **0xa1218
0004d7d0    // 把当前参数和上下文继续送入下一层公共分发出口。
0004d7d0  20013fd6   blr     x9
0004d7d4  a8634039   ldrb    w8, [fp, #0x18 {var_18}]
0004d7d8  1f0100f1   cmp     x8, #0
0004d7dc  e0079f1a   cset    w0, ne
0004d7e0  f44f42a9   ldp     x20, x19, [sp, #0x20] {__saved_x20} {__saved_x19}
0004d7e4  f50b40f9   ldr     x21, [sp, #0x10 {var_20}]
0004d7e8  fd7bc3a8   ldp     fp, lr, [sp], #0x30 {__saved_fp} {__saved_lr}
0004d7ec  c0035fd6   ret     

0004d7f0  c8020090   adrp    x8, 0xa5000
0004d7f4  08c11991   add     x8, x8, #0x670
0004d7f8  f30300aa   mov     x19, x0
0004d7fc  e00308aa   mov     x0, x8  {0xa5670}
0004d800  7dc20094   bl      sub_7e1f4
0004d804  e803002a   mov     w8, w0
0004d808  e00313aa   mov     x0, x19
0004d80c    // 检查初始化标志;未初始化时先走一次建表/取句柄流程。
0004d80c  08fdff34   cbz     w8, 0x4d7ac

0004d810  a8020090   adrp    x8, 0xa1000
0004d814  083141f9   ldr     x8, [x8, #0x260]  {0xa1260}
0004d818    // 从 **0xa1260
0004d818    // 取出一条运行时函数指针。这里只能静态看到
0004d818    // slot,看不到最终会跳到哪。
0004d818  150140f9   ldr     x21, [x8]
0004d81c  4fd4fe97   bl      sub_2958
0004d820  f40300aa   mov     x20, x0
0004d824  e1fbfff0   adrp    x1, 0xfffffffffffcc000
0004d828  21683991   add     x1, x1, #0xe5a  {-0x331a6}
0004d82c  a0630091   add     x0, fp, #0x18 {var_18}
0004d830  e2031f2a   mov     w2, wzr  {_start}
0004d834  c332ff97   bl      sub_1a340
0004d838  22bc9452   mov     w2, #0xa5e1
0004d83c  a1630091   add     x1, fp, #0x18 {var_18}
0004d840  e00314aa   mov     x0, x20  {0xa2820}
0004d844  4246a072   movk    w2, #0x232, lsl #0x10  {0x232a5e1}
0004d848    // 通过 x21
0004d848    // 间接调用初始化函数,说明这一层已经进入 slot
0004d848    // 驱动的动态分发。
0004d848  a0023fd6   blr     x21
0004d84c  f40300aa   mov     x20, x0
0004d850  a0630091   add     x0, fp, #0x18 {var_18}
0004d854  0d95ff97   bl      sub_32c88
0004d858  c0020090   adrp    x0, 0xa5000
0004d85c  00a01991   add     x0, x0, #0x668  {0xa5668}
0004d860    // 缓存初始化得到的句柄/上下文,后续调用会直接复用,不再重复建表。
0004d860  148400f8   str     x20, [x0], #0x8  {0xa5668}  {0xa5670}
0004d864  b5c20094   bl      sub_7e338
0004d868  e00313aa   mov     x0, x19
0004d86c  d0ffff17   b       0x4d7ac

此时已经可以根据运行的参数,计算出之后的调度链(大致会执行哪些函数都看一遍、结果发现是套娃),通过binary ninja继续跟,直到套娃:0x4ffd0 类似的分发函数,按照类似的思路去做:

function maybeInstallProcessDynamicDispatchProbe(label, address) {
  const p = ptr(address);
  const ownerModule = Process.findModuleByAddress(p);
  if (ownerModule === null || ownerModule.name !== MODULE_NAME) {
    return;
  }

  const relativeOffset = ptrToNumber(p) - ptrToNumber(ownerModule.base);
  if (!PROCESS_DYNAMIC_DISPATCH_TARGET_PROBE_OFFSETS.has(relativeOffset)) {
    return;
  }

重复这个操作下来两三次:0x4d7a8 -> 0x4ffd0 -> 0x4bd68 -> 0x4c8e4 -> 0x4e198 -> 0x4e548 -> 0x5bf18 -> 0x5b69c -> 0x5b5e0 -> 0x5b950。其中 0x5b818/0x5bcec 的 state dump 直接打出了 ChaCha20-like 常量、key、nonce,正式把整个处理逻辑给扒干净了,非常之搞笑。(脚本见末尾)

正好有运行时反汇编的操作,顺手就可以确定加密逻辑,并把flag推随机值的一起做了,验证之后是没问题的:(见样例源代码)

安全机制解析:

其实思路和2021还是2022的ACE保护差不多,都是游戏引擎侧对入口(global-meta.data、gdc)这种做加密,然后提高保护程序的分析难度,上混淆vmp、控制流平坦化什么的,这次赛题没有反调试、但是多环嵌套代码、自修改和动态跳转已经对Hook时机、方式有了一定的保护。同时三环的自修改写得也挺有意思,有外部分发、根据参数跳转执行的方式也对静态分析有了不小的挑战,同时动态分析时Frida的inlinehook是没法随意提前打上去拿各种信息的,stalker不知道为什么总是会崩。总之,很有意思!

解题优化思路:如果有动态trace多线程dump执行流的办法,其实这道题就变得非常简单了,用的frida脚本其实已经有了雏形,可能以后再继续研究看看有没有更好的办法去自动化吧。

打完决赛再放dump执行流的脚本,之后有时间可能改进一下做成工具,思路还是比较自信的,运行时dump听起来就很酷炫,这里多宣传一个个人写得zygisk工具:766K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6y4j5i4u0U0K9o6N6Q4x3X3c8U0L8$3c8W2i4K6u0r3h3Y4W2Y4K9i4y4C8d9h3&6B7k6h3y4@1i4@1g2r3i4@1u0o6i4K6S2o6i4@1f1$3i4K6R3@1i4K6W2r3i4@1f1#2i4K6R3#2i4@1t1@1i4@1f1^5i4@1t1$3i4@1p5K6i4@1f1%4i4K6W2m8i4K6R3@1i4@1f1#2i4@1p5@1i4@1p5%4i4@1f1@1i4@1u0p5i4@1q4o6i4@1f1#2i4K6S2r3i4@1q4r3i4@1f1@1i4@1u0n7i4@1p5#2i4@1f1%4i4K6W2o6i4K6S2n7i4@1f1%4i4K6W2o6i4K6S2n7i4@1g2r3i4@1u0o6i4K6S2o6i4@1f1#2i4K6R3H3i4K6W2r3i4@1f1&6i4K6R3&6i4@1t1@1i4@1f1^5i4@1q4q4i4@1u0m8i4@1f1#2i4K6W2p5i4K6W2n7i4@1f1#2i4K6R3$3i4K6R3#2i4@1f1#2i4@1p5@1i4@1p5%4i4@1f1@1i4@1u0p5i4@1q4o6i4@1f1%4i4K6W2m8i4K6R3@1i4@1f1$3i4K6R3K6i4@1t1K6i4@1f1$3i4@1t1K6i4K6V1#2i4@1g2r3i4@1u0o6i4K6W2m8M7$3g2U0j5$3!0E0j5i4m8Q4x3V1u0@1M7X3q4U0k6i4u0Q4c8e0g2Q4z5p5k6Q4b7f1k6Q4c8e0c8Q4b7V1u0Q4b7e0g2Q4c8e0g2Q4z5f1y4Q4b7e0S2Q4c8e0S2Q4b7V1k6Q4z5e0m8Q4c8e0S2Q4b7e0q4Q4z5p5y4Q4c8e0k6Q4z5e0N6Q4b7U0k6Q4c8e0k6Q4z5p5u0Q4b7e0k6Q4c8e0k6Q4z5o6S2Q4b7f1q4K6P5i4y4U0j5h3I4D9i4@1f1^5i4@1t1H3i4K6R3K6i4@1f1%4i4K6V1@1i4@1p5^5i4@1f1%4i4K6W2m8i4K6R3@1i4@1f1@1i4@1t1^5i4K6R3H3i4@1f1@1i4@1t1^5i4@1q4m8i4@1f1#2i4@1t1H3i4K6S2r3i4@1f1#2i4@1t1%4i4@1p5#2i4@1f1#2i4K6R3#2i4@1t1%4i4@1f1K6i4K6R3H3i4K6R3J5

脚本集:

见附件,逆向的加密算法:

用例:

frida -U -f com.tencent.ACE.gamesec2026.preliminary -l frida_move_trigger_sec2026.js
/*
 * Focused C port of tools/sec2026_process_chacha_replay.py
 *
 * Scope:
 *   1. GDScript text -> flag{sec2026_PART1_&lt;HEX&gt;}
 *   2. flag{sec2026_PART1_&lt;HEX&gt;} -> original random text
 *
 * Layer split:
 *   - game layer: GDScript xor_enc() and its inverse
 *   - type bridge: hex/flag formatting and parsing
 *   - native layer: runtime-captured ChaCha-like stream xor
 *
 * Build:
 *   gcc -O2 -std=c11 -Wall -Wextra -o tools/sec2026_process_chacha_replay.exe tools/sec2026_process_chacha_replay.c
 *
 * Examples:
 *   tools\\sec2026_process_chacha_replay.exe --gdscript-text c9854cf7
 *   tools\\sec2026_process_chacha_replay.exe --flag 201E5E4824337211
 *   tools\\sec2026_process_chacha_replay.exe --flag flag{sec2026_PART1_201E5E4824337211}
 */

#include &lt;ctype.h&gt;
#include &lt;stdint.h&gt;
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;

#define PROCESS_BYTES 8
#define FLAG_HEX_LEN 16
#define FLAG_PREFIX "flag{sec2026_PART1_"
#define FLAG_SUFFIX "}"

static const uint32_t RUNTIME_CONSTANTS[4] = {
    0x61707866u,
    0x3320646Fu,
    0x79622D31u,
    0x6B206573u,
};

static const uint32_t RUNTIME_KEY_WORDS[8] = {
    0x73316854u,
    0x20736C20u,
    0x2074306Eu,
    0x65722061u,
    0x6B203161u,
    0x21217965u,
    0x63657340u,
    0x36323032u,
};

static const uint32_t RUNTIME_COUNTER = 0x00000000u;

static const uint32_t RUNTIME_NONCE_WORDS[3] = {
    0x33323130u,
    0x37363534u,
    0x31303938u,
};

typedef struct Sec2026ProcessCipher {
    uint32_t state[16];
    size_t cursor;
    uint8_t keystream[64];
} Sec2026ProcessCipher;

/* -------------------------------------------------------------------------- */
/* shared helpers */
/* -------------------------------------------------------------------------- */

static uint32_t rotl32(uint32_t value, uint32_t shift) {
    return (value << shift) | (value >> (32u - shift));
}

static void bytes_to_hex_lower(const uint8_t *data, size_t length, char *out) {
    static const char table[] = "0123456789abcdef";
    size_t i;
    for (i = 0; i < length; ++i) {
        out[i * 2] = table[data[i] >> 4];
        out[i * 2 + 1] = table[data[i] & 0x0Fu];
    }
    out[length * 2] = '\0';
}

static void bytes_to_hex_upper(const uint8_t *data, size_t length, char *out) {
    static const char table[] = "0123456789ABCDEF";
    size_t i;
    for (i = 0; i < length; ++i) {
        out[i * 2] = table[data[i] >> 4];
        out[i * 2 + 1] = table[data[i] & 0x0Fu];
    }
    out[length * 2] = '\0';
}

static void bytes_to_ascii_preview(const uint8_t *data, size_t length, char *out) {
    size_t i;
    for (i = 0; i < length; ++i) {
        unsigned char value = data[i];
        out[i] = (value >= 0x20 && value <= 0x7E) ? (char)value : '.';
    }
    out[length] = '\0';
}

static int hex_value(int ch) {
    if (ch >= '0' && ch <= '9') {
        return ch - '0';
    }
    if (ch >= 'a' && ch <= 'f') {
        return 10 + (ch - 'a');
    }
    if (ch >= 'A' && ch <= 'F') {
        return 10 + (ch - 'A');
    }
    return -1;
}

static int parse_fixed_hex(const char *text, uint8_t *out, size_t out_len) {
    size_t i;
    for (i = 0; i < out_len; ++i) {
        int hi = hex_value((unsigned char)text[i * 2]);
        int lo = hex_value((unsigned char)text[i * 2 + 1]);
        if (hi < 0 || lo < 0) {
            return 0;
        }
        out[i] = (uint8_t)((hi << 4) | lo);
    }
    return text[out_len * 2] == '\0';
}

/* -------------------------------------------------------------------------- */
/* game layer: xor_enc() and inverse */
/* -------------------------------------------------------------------------- */

static void game_layer_text_to_process_input(const char *text, uint8_t out[PROCESS_BYTES]) {
    size_t length = strlen(text);
    size_t i;

    memset(out, 0, PROCESS_BYTES);
    if (length > PROCESS_BYTES) {
        length = PROCESS_BYTES;
    }
    memcpy(out, text, length);

    for (i = 0; i < PROCESS_BYTES - 1; ++i) {
        out[i] ^= out[i + 1];
    }
    out[PROCESS_BYTES - 1] ^= out[0];
}

static void game_layer_process_input_to_padded_text(const uint8_t in[PROCESS_BYTES], uint8_t out[PROCESS_BYTES]) {
    size_t i;
    uint8_t accum = 0;

    memset(out, 0, PROCESS_BYTES);
    for (i = 0; i < PROCESS_BYTES; ++i) {
        accum ^= in[i];
    }

    out[1] = accum;
    out[0] = (uint8_t)(in[0] ^ out[1]);
    for (i = 1; i < PROCESS_BYTES - 1; ++i) {
        out[i + 1] = (uint8_t)(in[i] ^ out[i]);
    }
}

/* -------------------------------------------------------------------------- */
/* native layer: runtime-captured stream xor */
/* -------------------------------------------------------------------------- */

static void quarter_round(uint32_t state[16], int a, int b, int c, int d) {
    state[a] += state[b];
    state[d] ^= state[a];
    state[d] = rotl32(state[d], 16);

    state[c] += state[d];
    state[b] ^= state[c];
    state[b] = rotl32(state[b], 12);

    state[a] += state[b];
    state[d] ^= state[a];
    state[d] = rotl32(state[d], 8);

    state[c] += state[d];
    state[b] ^= state[c];
    state[b] = rotl32(state[b], 7);
}

static void block_function(const uint32_t input[16], uint8_t out[64]) {
    uint32_t working[16];
    int round_index;
    int word_index;

    memcpy(working, input, sizeof(working));

    for (round_index = 0; round_index < 10; ++round_index) {
        quarter_round(working, 0, 4, 8, 12);
        quarter_round(working, 1, 5, 9, 13);
        quarter_round(working, 2, 6, 10, 14);
        quarter_round(working, 3, 7, 11, 15);

        quarter_round(working, 0, 5, 10, 15);
        quarter_round(working, 1, 6, 11, 12);
        quarter_round(working, 2, 7, 8, 13);
        quarter_round(working, 3, 4, 9, 14);
    }

    for (word_index = 0; word_index < 16; ++word_index) {
        uint32_t word = working[word_index] + input[word_index];
        out[word_index * 4] = (uint8_t)(word & 0xFFu);
        out[word_index * 4 + 1] = (uint8_t)((word >> 8) & 0xFFu);
        out[word_index * 4 + 2] = (uint8_t)((word >> 16) & 0xFFu);
        out[word_index * 4 + 3] = (uint8_t)((word >> 24) & 0xFFu);
    }
}

static void native_layer_init(Sec2026ProcessCipher *cipher) {
    memcpy(cipher->state + 0, RUNTIME_CONSTANTS, sizeof(RUNTIME_CONSTANTS));
    memcpy(cipher->state + 4, RUNTIME_KEY_WORDS, sizeof(RUNTIME_KEY_WORDS));
    cipher->state[12] = RUNTIME_COUNTER;
    memcpy(cipher->state + 13, RUNTIME_NONCE_WORDS, sizeof(RUNTIME_NONCE_WORDS));
    cipher->cursor = 64;
    memset(cipher->keystream, 0, sizeof(cipher->keystream));
}

static void native_layer_refill(Sec2026ProcessCipher *cipher) {
    block_function(cipher->state, cipher->keystream);
    cipher->cursor = 0;
    cipher->state[12] += 1u;
}

static void native_layer_apply_stream(Sec2026ProcessCipher *cipher, const uint8_t *input, size_t length, uint8_t *output) {
    size_t offset = 0;
    while (offset < length) {
        size_t i;
        size_t take;

        if (cipher->cursor >= 64) {
            native_layer_refill(cipher);
        }

        take = length - offset;
        if (take > 64 - cipher->cursor) {
            take = 64 - cipher->cursor;
        }

        for (i = 0; i < take; ++i) {
            output[offset + i] = (uint8_t)(input[offset + i] ^ cipher->keystream[cipher->cursor + i]);
        }

        cipher->cursor += take;
        offset += take;
    }
}

static void native_layer_forward_process(const uint8_t process_input[PROCESS_BYTES], uint8_t process_output[PROCESS_BYTES]) {
    Sec2026ProcessCipher cipher;
    native_layer_init(&cipher);
    native_layer_apply_stream(&cipher, process_input, PROCESS_BYTES, process_output);
}

static void native_layer_reverse_process(const uint8_t process_output[PROCESS_BYTES], uint8_t process_input[PROCESS_BYTES]) {
    Sec2026ProcessCipher cipher;
    native_layer_init(&cipher);
    native_layer_apply_stream(&cipher, process_output, PROCESS_BYTES, process_input);
}

/* -------------------------------------------------------------------------- */
/* type bridge: flag formatting and parsing */
/* -------------------------------------------------------------------------- */

static void type_bridge_process_output_to_flag_hex(const uint8_t process_output[PROCESS_BYTES], char out[FLAG_HEX_LEN + 1]) {
    bytes_to_hex_upper(process_output, PROCESS_BYTES, out);
}

static void type_bridge_process_output_to_full_flag(const uint8_t process_output[PROCESS_BYTES], char out[64]) {
    char hex[FLAG_HEX_LEN + 1];
    type_bridge_process_output_to_flag_hex(process_output, hex);
    sprintf(out, "%s%s%s", FLAG_PREFIX, hex, FLAG_SUFFIX);
}

static int type_bridge_extract_flag_hex(const char *input, char out[FLAG_HEX_LEN + 1]) {
    const char *start = input;
    const char *end = input + strlen(input);
    const char *body;
    const char *prefixless;
    size_t length;
    size_t i;

    while (start < end && isspace((unsigned char)*start)) {
        ++start;
    }
    while (end > start && isspace((unsigned char)end[-1])) {
        --end;
    }

    body = start;
    if ((size_t)(end - start) >= 6 && strncmp(start, "flag{", 5) == 0 && end[-1] == '}') {
        body = start + 5;
        end -= 1;
    }

    prefixless = body;
    if ((size_t)(end - body) >= strlen("sec2026_PART1_") &&
        strncmp(body, "sec2026_PART1_", strlen("sec2026_PART1_")) == 0) {
        prefixless = body + strlen("sec2026_PART1_");
    }

    length = (size_t)(end - prefixless);
    if (length != FLAG_HEX_LEN) {
        return 0;
    }

    for (i = 0; i < FLAG_HEX_LEN; ++i) {
        if (hex_value((unsigned char)prefixless[i]) < 0) {
            return 0;
        }
        out[i] = (char)toupper((unsigned char)prefixless[i]);
    }
    out[FLAG_HEX_LEN] = '\0';
    return 1;
}

/* -------------------------------------------------------------------------- */
/* cli */
/* -------------------------------------------------------------------------- */

static void print_usage(const char *program_name) {
    fprintf(stderr,
        "Usage:\n"
        "  %s --gdscript-text &lt;text&gt;\n"
        "  %s --flag <16hex|flag{sec2026_PART1_<16hex>}>\n",
        program_name,
        program_name);
}

static void print_forward_result(const char *text) {
    uint8_t process_input[PROCESS_BYTES];
    uint8_t process_output[PROCESS_BYTES];
    char process_input_hex[PROCESS_BYTES * 2 + 1];
    char process_output_hex[PROCESS_BYTES * 2 + 1];
    char process_input_ascii[PROCESS_BYTES + 1];
    char full_flag[64];

    game_layer_text_to_process_input(text, process_input);
    native_layer_forward_process(process_input, process_output);

    bytes_to_hex_lower(process_input, PROCESS_BYTES, process_input_hex);
    bytes_to_hex_upper(process_output, PROCESS_BYTES, process_output_hex);
    bytes_to_ascii_preview(process_input, PROCESS_BYTES, process_input_ascii);
    type_bridge_process_output_to_full_flag(process_output, full_flag);

    printf("game_layer.input_text = '%s'\n", text);
    printf("game_layer.process_input_hex = %s\n", process_input_hex);
    printf("game_layer.process_input_ascii = %s\n", process_input_ascii);
    printf("native_layer.process_output_hex = %s\n", process_output_hex);
    printf("type_bridge.flag = %s\n", full_flag);
}

static void print_reverse_result(const char *flag_text) {
    char flag_hex[FLAG_HEX_LEN + 1];
    uint8_t process_output[PROCESS_BYTES];
    uint8_t process_input[PROCESS_BYTES];
    uint8_t padded_text[PROCESS_BYTES];
    size_t text_length = PROCESS_BYTES;
    char process_input_hex[PROCESS_BYTES * 2 + 1];
    char padded_text_hex[PROCESS_BYTES * 2 + 1];
    char recovered_text[PROCESS_BYTES + 1];

    if (!type_bridge_extract_flag_hex(flag_text, flag_hex)) {
        fprintf(stderr, "invalid flag value: %s\n", flag_text);
        exit(1);
    }
    if (!parse_fixed_hex(flag_hex, process_output, PROCESS_BYTES)) {
        fprintf(stderr, "failed to parse flag hex: %s\n", flag_hex);
        exit(1);
    }

    native_layer_reverse_process(process_output, process_input);
    game_layer_process_input_to_padded_text(process_input, padded_text);

    while (text_length > 0 && padded_text[text_length - 1] == 0) {
        --text_length;
    }

    memcpy(recovered_text, padded_text, text_length);
    recovered_text[text_length] = '\0';

    bytes_to_hex_lower(process_input, PROCESS_BYTES, process_input_hex);
    bytes_to_hex_lower(padded_text, PROCESS_BYTES, padded_text_hex);

    printf("type_bridge.flag_hex = %s\n", flag_hex);
    printf("native_layer.process_input_hex = %s\n", process_input_hex);
    printf("game_layer.recovered_padded_hex = %s\n", padded_text_hex);
    printf("game_layer.recovered_text = '%s'\n", recovered_text);
}

int main(int argc, char **argv) {
    if (argc != 3) {
        print_usage(argv[0]);
        return 1;
    }

    if (strcmp(argv[1], "--gdscript-text") == 0) {
        print_forward_result(argv[2]);
        return 0;
    }

    if (strcmp(argv[1], "--flag") == 0) {
        print_reverse_result(argv[2]);
        return 0;
    }

    print_usage(argv[0]);
    return 1;
}

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

上传的附件:
收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回