首页
社区
课程
招聘
[原创]2026腾讯游戏安全PC初赛Writeup
发表于: 5小时前 143

[原创]2026腾讯游戏安全PC初赛Writeup

5小时前
143

四年参赛老兵报道了。。

题目描述

(1) 成功加载驱动并与之正确通信,理解题目基本机制,识别并排除干扰信息。需在 writeup 中说明分析过程。(满分1.5分)

(2) 发现「宫殿」系统的隐匿通信手段并编写可工作的检测工具,发现的手段种类越多、利用越完整,得分越高,需提交相关源码并在 writeup 中说明分析过程(满分5.0分)

(3) 探索出「宫殿」系统完整的迷宫墙壁布局,需提交迷宫地图和自动化探索脚本。(满分1.5分)

(4) 在还原的迷宫地图上求解起点到终点的最短路径。需提交求解算法和路径结果。(满分0.5分)

(5) 提交正确的 Flag 得 1.5 分;到达终点但未能正确解密得 0.5 分。(满分1.5分)

本篇分析按照 1->3->4->5->2 的顺序分析。

加载驱动并通信

首先分析 R3 程序,字符串大法好

直接就能看到一个驱动通信的设备:

  • \\.\ShadowGate

同时还看到两个全局事件:

  • Global\MazeMoveOK
  • Global\MazeMoveWall

信息获取完,直接看驱动程序,顺着 DriverEntry 找到关键逻辑,由于 sub_1400018A0vm 了,所以它被识别为了 no return,取消掉就可以看清楚所有逻辑了。

具体通信设备就是 \\.\ShadowGate,开始分配了一个 0x1D8 大小的内存,指针保存在全局变量 P 中,tagMaze

往下翻可以发现驱动是通过 IRP 通信的

具体就是 R3 程序通过 DeviceIoControl 与驱动通信。

探索地图&求解最短路

查看 DeviceControl 函数,有三个控制码

经过逆向分析,猜测出 P 的结构。

0x00~0xA8 169B 13×13 迷宫网格(0=通行,1=墙壁)
偏移 大小 说明
0xAC~0xB3 8B 当前坐标 (x, y)
0xB4~0xBB 8B 其他状态
0xBC~0xBF 4B 总移动计数(每次 checksum 通过的 IOCTL 递增)
0xC0 1B 状态标志
0x1C0 8B SpinLock
0x1C8~0x1CF 8B 调用进程 PID
0x1D0~0x1D7 8B 调用线程 TID

对应以下结构体:

struct Maze
{
    UCHAR maze[172];
    struct Pos Position;
    UCHAR OtherState[8];
    UINT32 MoveCount;
    UCHAR StatusFlag;
    UCHAR Reserved1[255];
    UINT64 SpinLock;
    UINT64 ProcessId;
    UINT64 ThreadId;
};

恢复结构体之后,分析得到三个控制码的功能:

IOCTL Code 功能 输入 输出
0x8001200C 获取迷宫信息 24字节:width/height/entry_x/entry_y/exit_x/exit_y(均DWORD)
0x80012008 重置到入口
0x80012004 移动操作 12字节 132字节

它默认识别的有一点问题,稍微改名改类型之后主要分析移动操作。

显然输入的时候还有一个 checksum,假设输入结构为:

struct input{
    int x;
    int y;
    int z;
}

那么要求 x ^ y ^ 0xDEAD1337 == z

sub_140002161 的函数是被 V 了的,不好分析,但是它把迷宫结构体和 x 加密得到的 v22 传进了函数中,那么可以断定,输入基本上就只看前四个字节。

起应用层的调试器验证一下,断 DeviceIoControl,寻找第三个参数(输入buffer)的地址。

输入 w 得到

x = 0x52
y = 0
z = 0xDEAD1365
x ^ y ^ 0xDEAD1337 == z
# True

那么这里就有一个不吃操作的打法,上下左右都按一遍就能得到操作的 buffer,直接用。

方向 按键 编码后的 x
UP W/I 0x52
DOWN S/K 0xD3
LEFT A/J 0x53
RIGHT D/L 0xD0

拿到之后,它都提示 13*13 的棋盘了,果断猜迷宫就在 0 偏移上。

有点像,拿到直接解析成迷宫的格式。

.......#.....
######.###.#.
.....#.....#.
.###.#######.
.#.........#.
.#.#.#####.#.
.#.#.#...#.#.
.#.###.#.###.
.#.....#.#...
.#######.#.##
...#...#.#.#.
##.#.#.#.#.#.
.....#.#.....

起点 (0,0),终点 (12,12),显然有一条最短路的,数据化一下,然后写 BFS 脚本:

from collections import deque

maze = [
    [0,0,0,0,0,0,0,1,0,0,0,0,0],
    [1,1,1,1,1,1,0,1,1,1,0,1,0],
    [0,0,0,0,0,1,0,0,0,0,0,1,0],
    [0,1,1,1,0,1,1,1,1,1,1,1,0],
    [0,1,0,0,0,0,0,0,0,0,0,1,0],
    [0,1,0,1,0,1,1,1,1,1,0,1,0],
    [0,1,0,1,0,1,0,0,0,1,0,1,0],
    [0,1,0,1,1,1,0,1,0,1,1,1,0],
    [0,1,0,0,0,0,0,1,0,1,0,0,0],
    [0,1,1,1,1,1,1,1,0,1,0,1,1],
    [0,0,0,1,0,0,0,1,0,1,0,1,0],
    [1,1,0,1,0,1,0,1,0,1,0,1,0],
    [0,0,0,0,0,1,0,1,0,0,0,0,0],
]

dx = [0, 0, -1, 1]  # UP, DOWN, LEFT, RIGHT
dy = [-1, 1, 0, 0]

q = deque([((0,0), [])])
visited = {(0,0)}
while q:
    (cx,cy), path = q.popleft()
    if (cx,cy) == (12,12):
        print("Path:", "".join("WSAD"[d] for d in path))
        break
    for d in range(4):
        nx, ny = cx+dx[d], cy+dy[d]
        if 0<=nx<13 and 0<=ny<13 and (nx,ny) not in visited and maze[ny][nx]==0:
            visited.add((nx,ny))
            q.append(((nx,ny), path+[d]))

得到路径:DDDDDDSSDDDDWWDDSSSSSSSSAASSSSDD

依次输入,可以获得 flag

所以拿到 flag:flag{SHAD0WNT_HYPERVMX}

系统隐蔽的通信手段

隐蔽的通信手段一共有五种,前两种在字符串那边就盯真了。

  • 命名事件对象
  • 命名信号量
  • TEB隐藏字段/LastError
  • PEB字段+句柄保护标志
  • IOCTL输出缓冲区/时间

命名事件对象

发现过程: EXE 字符串表直接暴露 Global\MazeMoveOKGlobal\MazeMoveWall。逆向 EXE sub_140001340

hEventOK   = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveOK");
hEventWall = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveWall");

驱动端实现(sub_1400022B0): 使用 ZwOpenEvent + ZwSetEvent 在内核态信号化事件:

if (!result || result == 2)  // 0=成功 -> OK事件, 2=撞墙 -> Wall事件
    name = L"\\BaseNamedObjects\\MazeMoveOK";
else
    name = L"\\BaseNamedObjects\\MazeMoveWall";
ZwOpenEvent(&hEvent, EVENT_MODIFY_STATE, &objAttrs);
ZwSetEvent(hEvent, NULL);
ZwClose(hEvent);

检测代码:

ResetEvent(hEvtOK);
ResetEvent(hEvtWall);
// ... 发送移动 IOCTL ...
if (WaitForSingleObject(hEvtOK, 100) == WAIT_OBJECT_0)  // 移动成功
if (WaitForSingleObject(hEvtWall, 100) == WAIT_OBJECT_0) // 撞墙

命名信号量

发现过程: EXE sub_14021B91F 中通过 SSE 指令(_mm_xor_ps)批量 XOR 解码字符串后调用 CreateSemaphoreW。密钥为 0x004B(wide char),数据存储在 EXE 0x140003BE0~0x140003C300x140003B80~0x140003BD0

解码结果:

OK信号量:   Global\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}
Wall信号量: Global\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}

驱动端确认(sub_140319A37): 驱动同样存储 XOR 0x4B 编码的名称(0x1400041600x1400041E0),通过 ObReferenceObjectByName 获取信号量内核对象后调用 KeReleaseSemaphore

检测代码:

hSemOK = CreateSemaphoreW(NULL, 0, 16,
    L"Global\\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}");
// 移动前排空
while (WaitForSingleObject(hSemOK, 0) == WAIT_OBJECT_0);
// ... 发送移动 IOCTL ...
if (WaitForSingleObject(hSemOK, 100) == WAIT_OBJECT_0) // 移动成功

TEB 字段/LastError

驱动端实现(sub_140316ADF): 完整的跨进程 TEB 写入:

这里有两种检测方法,首先是 _TEB + 0x68 保存的值是 LastError

kd> dt _TEB
nt!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x038 EnvironmentPointer : Ptr64 Void
   +0x040 ClientId         : _CLIENT_ID
   +0x050 ActiveRpcHandle  : Ptr64 Void
   +0x058 ThreadLocalStoragePointer : Ptr64 Void
   +0x060 ProcessEnvironmentBlock : Ptr64 _PEB
   +0x068 LastErrorValue   : Uint4B
   +0x06c CountOfOwnedCriticalSections : Uint4B
   +0x070 CsrClientThread  : Ptr64 Void

因此可以在用户层调用 GetLastError 进行通信检测,当然,直接用户层读取该字段也可以,两个检测方法本质是一样的。

GetLastError() == (int)0xC0DE0001; // 方法1

BYTE* teb = (BYTE*)__readgsqword(0x30);
val = *(int*)(teb + 0x68);
val == (int)0xC0DE0001; // 方法2

TEB+句柄通信

注意到驱动中的函数

观察后续汇编可发现,应用层可以向 TEB+0x1748 传入一个 HANDLE,然后供内核调用

ZwSetInformationObject(
    h,
    ObjectHandleFlagInformation,  // 4
    &info,
    sizeof(info)                 // 2
);

因此可以把 HANDLE 放到 TEB+0x1748 中,观察是否被设置 HANDLE_FLAG_PROTECT_FROM_CLOSE

时间

讲道理感觉最后一种有可能是和这个相关,但是多次测试没有发现特别明显的规律。

总结

下面是我总共的代码,可以跑出 flag,并且使用四种方式成功检测到移动成功的信息。

#include &lt;windows.h&gt;
#include &lt;stdio.h&gt;
#include &lt;string.h&gt;

#define IOCTL_INFO  0x8001200Cu
#define IOCTL_RESET 0x80012008u
#define IOCTL_MOVE  0x80012004u
#define WIN_MAGIC   0x57494E21u

static const BYTE ENC_UP = 0x52, ENC_DOWN = 0xD3, ENC_LEFT = 0x53, ENC_RIGHT = 0xD0;

static HANDLE hDev;
static HANDLE hEvtOK, hEvtWall, hSemOK, hSemWall;

static int dev_open(void) {
    hDev = CreateFileW(L"\\\\.\\ShadowGate",
        GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hDev == INVALID_HANDLE_VALUE) {
        printf("[!] open fail %lu\n", GetLastError());
        return 0;
    }
    printf("[+] Device opened\n");
    return 1;
}

static void dev_reset(void) {
    DWORD br = 0;
    DeviceIoControl(hDev, IOCTL_RESET, NULL, 0, NULL, 0, &br, NULL);
}

static double last_move_ms = 0.0;

static int dev_move(BYTE enc, BYTE out[132], DWORD* pbr) {
    BYTE in_buf[12];
    memset(in_buf, 0, 12);
    in_buf[0] = enc;
    DWORD ck = (DWORD)(enc ^ 0xDEAD1337u);
    memcpy(in_buf + 8, &ck, 4);
    memset(out, 0, 132);
    *pbr = 0;
    LARGE_INTEGER t0, t1, freq;
    QueryPerformanceFrequency(&freq);
    QueryPerformanceCounter(&t0);
    int ret = DeviceIoControl(hDev, IOCTL_MOVE, in_buf, 12, out, 132, pbr, NULL);
    QueryPerformanceCounter(&t1);
    last_move_ms = (double)(t1.QuadPart - t0.QuadPart) * 1000.0 / freq.QuadPart;
    printf("    [dev_move] Enc=0x%02X, Time=%.2fms, BytesReturned=%lu\n", enc, last_move_ms, *pbr);
    return ret;
}

static void ch_init(void) {
    hEvtOK = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveOK");
    hEvtWall = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveWall");
    hSemOK = CreateSemaphoreW(NULL, 0, 16, L"Global\\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}");
    hSemWall = CreateSemaphoreW(NULL, 0, 16, L"Global\\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}");
}

static int read_teb_field(void) {
    int val = 0;
    __try {
        BYTE* teb = (BYTE*)__readgsqword(0x30);
        val = *(int*)(teb + 0x68);
    }
    __except (1) { val = 0; }
    return val;
}

static void clear_teb_field(void) {
    __try {
        BYTE* teb = (BYTE*)__readgsqword(0x30);
        *(int*)(teb + 0x68) = 0;
    }
    __except (1) {}
}

static void set_teb_handle() {
    BYTE* teb = (BYTE*)__readgsqword(0x30);
    *(HANDLE *)(teb + 0x1748) = hDev; 
}

static int check_ok(void) {
    int found = 0;

    /* Wait for driver to finish processing (random delay up to 60ms) */
    Sleep(150);

    /* Channel 1: Named Event - non-blocking check */
    if (!found && hEvtOK && WaitForSingleObject(hEvtOK, 100) == WAIT_OBJECT_0)
        found = 1;

    /* Channel 2: Named Semaphore - non-blocking check */
    if (!found && hSemOK && WaitForSingleObject(hSemOK, 100) == WAIT_OBJECT_0)
        found = 2;

    /* Channel 3: TEB hidden field (TEB+0x68) */
    if (!found && GetLastError() == (int)0xC0DE0001) // 或者使用 read_teb_field() 判断,等效
        found = 3;

    DWORD handle_flags = 0;
    GetHandleInformation(hDev, &handle_flags);
    if (HANDLE_FLAG_PROTECT_FROM_CLOSE == (handle_flags & HANDLE_FLAG_PROTECT_FROM_CLOSE)) {
        found = 4;
    }

    return found;
}

static void ch_pre(void) {
    if (hEvtOK) ResetEvent(hEvtOK);
    if (hEvtWall) ResetEvent(hEvtWall);
    if (hSemOK) while (WaitForSingleObject(hSemOK, 0) == WAIT_OBJECT_0);
    if (hSemWall) while (WaitForSingleObject(hSemWall, 0) == WAIT_OBJECT_0);
    SetHandleInformation(hDev, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);
    clear_teb_field();
}

int main(void) {
    printf("============================================================\n");
    printf("  ShadowGate Final Solver\n");
    printf("============================================================\n\n");

    if (!dev_open()) return 1;
    ch_init();
    set_teb_handle();
    /* Known shortest path: DDDDDDSSDDDDWWDDSSSSSSSSAASSSSDD (32 steps) */
    /* D=RIGHT(0xD0), S=DOWN(0xD3), W=UP(0x52), A=LEFT(0x53) */
    const char* path_str = "DDDDDDSSDDDDWWDDSSSSSSSSAASSSSDD";
    int path_len = (int)strlen(path_str);

    BYTE path_enc[64];
    for (int i = 0; i < path_len; i++) {
        switch (path_str[i]) {
        case 'W': path_enc[i] = ENC_UP;    break;
        case 'S': path_enc[i] = ENC_DOWN;  break;
        case 'A': path_enc[i] = ENC_LEFT;  break;
        case 'D': path_enc[i] = ENC_RIGHT; break;
        }
    }

    printf("[+] Maze: 13x13, Entry=(0,0), Exit=(12,12)\n");
    printf("[+] Shortest path (%d steps): %s\n\n", path_len, path_str);

    /* Reset and walk the path */
    dev_reset();
    printf("[*] Walking path...\n");

    char flag[256] = { 0 };
    int got_flag = 0;

    for (int i = 0; i < path_len; i++) {
        ch_pre();
        BYTE out[132]; DWORD br = 0;
        dev_move(path_enc[i], out, &br);

        int ok = check_ok();

        /* Check WIN magic */
        DWORD magic = 0;
        if (br >= 0x84) memcpy(&magic, out + 0x3C, 4);
        if (magic == WIN_MAGIC) {
            DWORD cl = 0;
            memcpy(&cl, out + 0x80, 4);
            if (cl > 0 && cl <= 63) {
                memcpy(flag, out + 0x40, cl);
                flag[cl] = 0;
                got_flag = 1;
            }
        }

        printf("  Step %2d: %c (enc=0x%02X) -> %s%d%s\n",
            i, path_str[i], path_enc[i],
            ok ? "OK" : "??",
            ok,
            (magic == WIN_MAGIC) ? " *** WIN! ***" : "");

        if (got_flag) break;
    }

    if (got_flag) {
        printf("\n============================================================\n");
        printf("  FLAG: %s\n", flag);
        printf("============================================================\n");
        FILE* f = fopen("flag.txt", "w");
        if (f) { fprintf(f, "%s\n", flag); fclose(f); printf("[+] Saved flag.txt\n"); }
    }

    /* Print maze map */
    printf("\n[*] === Maze Map (0=passable, 1=wall) ===\n\n");
    /* Hardcoded from memory dump */
    static const BYTE maze[169] = {
        0,0,0,0,0,0,0,1,0,0,0,0,0,
        1,1,1,1,1,1,0,1,1,1,0,1,0,
        0,0,0,0,0,1,0,0,0,0,0,1,0,
        0,1,1,1,0,1,1,1,1,1,1,1,0,
        0,1,0,0,0,0,0,0,0,0,0,1,0,
        0,1,0,1,0,1,1,1,1,1,0,1,0,
        0,1,0,1,0,1,0,0,0,1,0,1,0,
        0,1,0,1,1,1,0,1,0,1,1,1,0,
        0,1,0,0,0,0,0,1,0,1,0,0,0,
        0,1,1,1,1,1,1,1,0,1,0,1,1,
        0,0,0,1,0,0,0,1,0,1,0,1,0,
        1,1,0,1,0,1,0,1,0,1,0,1,0,
        0,0,0,0,0,1,0,1,0,0,0,0,0
    };
    for (int y = 0; y < 13; y++) {
        printf("  ");
        for (int x = 0; x < 13; x++) {
            if (x == 0 && y == 0) printf("S");
            else if (x == 12 && y == 12) printf("E");
            else printf("%c", maze[y * 13 + x] ? '#' : ' ');
        }
        printf("\n");
    }

    CloseHandle(hEvtOK); CloseHandle(hEvtWall);
    if (hSemOK) CloseHandle(hSemOK);
    if (hSemWall) CloseHandle(hSemWall);
    CloseHandle(hDev);
    printf("\n[*] Done.\n");
    return 0;
}

通信机制总结

题目通过 \\.\ShadowGate 设备,使用 DeviceIoControl 进行通信。

IOCTL Code 功能 输入 输出
0x8001200C 获取迷宫信息 24字节:width/height/entry_x/entry_y/exit_x/exit_y(均DWORD)
0x80012008 重置到入口
0x80012004 移动操作 12字节 132字节

题目使用五种隐蔽的方式告诉应用层本次移动是撞墙还是可通行,驱动内部由全局变量 dword_140005004 来决定本次使用什么信道通信。最开始的时候一定是 1 2 3 4 5 的顺序,后续的通信判定似乎是随机,具体没有研究出来。


[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

最后于 5小时前 被xi@0ji233编辑 ,原因:
收藏
免费 1
支持
分享
最新回复 (2)
雪    币: 1859
活跃值: (1910)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
2
牛逼
5小时前
0
雪    币: 2450
活跃值: (1740)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
3
还能这么搞
5小时前
0
游客
登录 | 注册 方可回帖
返回