xia0ji233 发表于 2024-4-15 01:03

腾讯游戏安全大赛2024初赛题解

报名参加了一下2024的游戏安全竞赛,今天初赛结束,总体来说赛题质量还是非常高的。

[解题附件下载](https://xia0ji233.pro/2024/04/15/tencent-race-2024-pre/attachment.zip)



吐槽一下,吾爱官方开个2024的分类吧

<!--more-->

## 2024 腾讯游戏安全大赛初赛

### 参赛人员信息



### 分析

#### 找到内存中的两串token,作为答案提交(2分)。

先工具分析一下,发现加了 VM,动调发现对很多工具有检测,部分改名可以直接绕过,但是 CE 怎么改都会被检测,所以先上微步基本分析一下行为:

https://s.threatbook.com/report/file/1bc2f607b5e4707a70a32bb78ac72c9b895f00413ba4bd21229f6103757ca19f

注意到了有注入行为,一般会通过 `WriteProcessMemory` 这个 API 进行,于是编写 DLL 去hook看看情况。

```C
// dllmain.cpp : 定义 DLL 应用程序的入口点。 hack.dll
#include "pch.h"
#include <Windows.h>
#include <stdio.h>
#include<Psapi.h>
typedef BOOL (*Func)( HANDLE hProcess,
   LPVOID lpBaseAddress,
   LPCVOID lpBuffer,
   SIZE_T nSize,
   SIZE_T* lpNumberOfBytesWritten
);
Func OriginFunc = NULL;

BYTE HookCode[] = {
    0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //mov rax , xxx
    0xFF,0xE0                                          //jmp rax
};
BYTE OriginCode;
SIZE_T HookLen = sizeof(HookCode);
DWORD saved=0;
WCHAR FILENAME;
BOOL HackWriteProcessMemory(HANDLE hProcess,
    LPVOID lpBaseAddress,
    LPCVOID lpBuffer,
    SIZE_T nSize,
    SIZE_T* lpNumberOfBytesWritten
) {
   
    VirtualProtect(OriginFunc, HookLen, PAGE_EXECUTE_READWRITE, &saved);
    memcpy(OriginFunc, OriginCode,HookLen);             //unhook
    printf("Call WriteProcessMemory(%p,%p,%p,%d,%p)",hProcess,lpBaseAddress,lpBuffer,nSize,lpNumberOfBytesWritten);
    GetModuleFileNameEx(hProcess, NULL, FILENAME, MAX_PATH);
    wprintf(L"ProcessName=%s\n", FILENAME);
    BOOL ret=OriginFunc(hProcess, lpBaseAddress, lpBuffer, nSize,lpNumberOfBytesWritten);
    memcpy(OriginFunc, HookCode, HookLen);               //rehook
    VirtualProtect(OriginFunc, HookLen, saved, &saved);
    return ret;
}

void hack() {
    OriginFunc = WriteProcessMemory;
    VirtualProtect(OriginFunc, HookLen, PAGE_EXECUTE_READWRITE, &saved);
    memcpy(OriginCode, OriginFunc,HookLen); //saved
    *(__int64*)(HookCode + 2) = (__int64)HackWriteProcessMemory;//build
    memcpy(OriginFunc, HookCode, HookLen);//Hook
    VirtualProtect(OriginFunc, HookLen, saved, &saved);
    printf("Hook done\n");
}


BOOL APIENTRY DllMain( HMODULE hModule,
                     DWORDul_reason_for_call,
                     LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH: {
      AllocConsole();
      freopen("CONOUT$", "w", stdout);
      hack();
    }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
      break;
    }
    return TRUE;
}
```

这一块没什么进展,但是突然发现这个进程对其它进程有些操作



这不就是 token1 嘛,但是发现底下没有这个文件,尝试创建这个文件,发现直接就有内容写进去了,用命令可以打印出来。




于是 token1 就出来了,很神奇。 `token1: 757F4749AEBB1891EF5AC2A9B5439CEA`。

对于token2,一直尝试做一些 API 的 hook 想看看它干了啥,这里我做了 MmCopyMemory 的 hook。

```C
#include <ntifs.h>
#include <ntdef.h>
#include <ntstatus.h>
#include <ntddk.h>
#define MAX_BACKTRACE_DEPTH 20
#define SYMBOL L"\\??\\xia0ji2333"
#define kprintf(format, ...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, format, ##__VA_ARGS__)

UINT64 BaseAddr=NULL, DLLSize=0;

NTSTATUS CreateDevice(PDEVICE_OBJECT driver) {
    NTSTATUS status;
    UNICODE_STRING MyDriver;
    PDEVICE_OBJECT device = NULL;
    RtlInitUnicodeString(&MyDriver, L"\\DEVICE\\xia0ji233");
    status = IoCreateDevice(
      driver,
      sizeof(driver->DeviceExtension),
      &MyDriver,
      FILE_DEVICE_UNKNOWN,
      FILE_DEVICE_SECURE_OPEN,
      FALSE,
      &device
    );
    if (status == STATUS_SUCCESS) {
      UNICODE_STRING Sym;
      RtlInitUnicodeString(&Sym, SYMBOL);
      status = IoCreateSymbolicLink(&Sym, &MyDriver);
      if (status == STATUS_SUCCESS) {
            kprintf(("Line %d:xia0ji233: symbol linked success\n"), __LINE__);
      }
      else {
            kprintf(("Line %d:xia0ji233: symbol linked failed status=%x\n"), __LINE__, status);
      }
    }
    else {
      kprintf(("Line %d:xia0ji233: create device fail status=%x\n"), __LINE__, status);
    }
}

void DeleteDevice(PDRIVER_OBJECT pDriver) {
    kprintf(("Line %d:xia0ji233: start delete device\n"), __LINE__);
    if (pDriver->DeviceObject) {
      UNICODE_STRING Sym;
      RtlInitUnicodeString(&Sym, SYMBOL);//CreateFile
      kprintf(("Line %d:xia0ji233: Delete Symbol\n"), __LINE__);
      IoDeleteSymbolicLink(&Sym);
      kprintf(("Line %d:xia0ji233: Delete Device\n"), __LINE__);
      IoDeleteDevice(pDriver->DeviceObject);
    }
    kprintf(("Line %d:xia0ji233: end delete device\n"), __LINE__);
}



char newcode[] = {
    0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//mov rax,xxx
    0xFF,0xE0//jmp rax
};
char oldcode[] = {
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,
};

char *target;

KIRQL WPOFFx64()
{
    KIRQL irql = KeRaiseIrqlToDpcLevel();
    UINT64 cr0 = __readcr0();
    cr0 &= 0xfffffffffffeffff;
    __writecr0(cr0);
    _disable();
    return irql;
}

void WPONx64(KIRQL irql)
{
    UINT64 cr0 = __readcr0();
    cr0 |= 0x10000;
    _enable();
    __writecr0(cr0);
    KeLowerIrql(irql);
}

NTSTATUS Unhook() {
    KIRQL irql = WPOFFx64();
    for (int i = 0; i < sizeof(newcode); i++) {
      target = oldcode;
    }
    WPONx64(irql);
    return STATUS_SUCCESS;
}

NTSTATUS Hook() {
    KIRQL irql = WPOFFx64();
    for (int i = 0; i < sizeof(newcode); i++) {
      target = newcode;
    }
    WPONx64(irql);
    return STATUS_SUCCESS;
}
typedef NTSTATUS(*Copy)(PVOID, MM_COPY_ADDRESS, SIZE_T, ULONG, SIZE_T *);

PDRIVER_OBJECT g_Object = NULL;
typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;
    PVOID EntryPoint;//驱动的进入点 DriverEntry
    ULONG SizeOfImage;
    UNICODE_STRING FullDllName;//驱动的满路径
    UNICODE_STRING BaseDllName;//不带路径的驱动名字
    ULONG Flags;
    USHORT LoadCount;
    USHORT TlsIndex;
    union {
      LIST_ENTRY HashLinks;
      struct {
            PVOID SectionPointer;
            ULONG CheckSum;
      };
    };
    union {
      struct {
            ULONG TimeDateStamp;
      };
      struct {
            PVOID LoadedImports;
      };
    };
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

VOID bianliqudongmokuai(PUNICODE_STRING name, UINT64* pBaseAddr,UINT64* pSize)
{
    LDR_DATA_TABLE_ENTRY *TE, *Tmp;
    TE = (LDR_DATA_TABLE_ENTRY*)g_Object->DriverSection;
    PLIST_ENTRY LinkList;
    ;
    int i = 0;
    LinkList = TE->InLoadOrderLinks.Flink;
    while (LinkList != &TE->InLoadOrderLinks)
    {
      Tmp = (LDR_DATA_TABLE_ENTRY*)LinkList;
      if (RtlCompareUnicodeString(&Tmp->BaseDllName, name, FALSE))
      {
      }
      else
      {
            kprintf(("Found Module!\n"));
            *pBaseAddr = (UINT64)(Tmp->DllBase);
            *pSize = (UINT64)(Tmp->SizeOfImage);
      }
      LinkList = LinkList->Flink;
      i++;
    }


}


NTSTATUS
    myMmCopyMemory(
    _In_ PVOID TargetAddress,
    _In_ MM_COPY_ADDRESS SourceAddress,
    _In_ SIZE_T NumberOfBytes,
    _In_ ULONG Flags,
    _Out_ PSIZE_T NumberOfBytesTransferred
) {

    if (!BaseAddr) {
      UNICODE_STRING name;
      RtlInitUnicodeString(&name, L"ace.sys");
      bianliqudongmokuai(&name,&BaseAddr,&DLLSize);
      if (!BaseAddr) {
            goto end;
      }
    }


    PVOID backtrace;
    USHORT capturedFrames = RtlCaptureStackBackTrace(0, MAX_BACKTRACE_DEPTH, backtrace, NULL);
    UINT64 addr = BaseAddr;
    UINT64 size = DLLSize;
    int flag = 0;

    for (USHORT i = 0; i < capturedFrames; i++)
    {
      if (backtrace >= addr && backtrace <= addr + size) {
            flag = 1;
      }
    }
    if (flag) {
      kprintf(("xia0ji233: calls MmCopyMemory(%p,%p,%d,%p,%p)\n"), TargetAddress, SourceAddress, NumberOfBytes, Flags, NumberOfBytesTransferred);
      kprintf(("Here is data: "));
      for (INT64 i = 0; i < NumberOfBytes; i++) {
            kprintf(("%02x "), *((unsigned char*)SourceAddress.VirtualAddress + i));
      }
      kprintf(("\n"));
    }


    end:
    Unhook();
    Copy func = (Copy)target;
    NTSTATUS s = func(TargetAddress, SourceAddress, NumberOfBytes, Flags, NumberOfBytesTransferred);
    Hook();

    return s;
}

void DriverUnload(PDRIVER_OBJECT pDriver) {
    kprintf(("Line %d:xia0ji233: start unload\n"), __LINE__);
    Unhook();
    DeleteDevice(pDriver);
}


NTSTATUS DriverEntry(
    _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath
) {
    DriverObject->DriverUnload = DriverUnload;
    CreateDevice(DriverObject);
    kprintf(("Line %d:xia0ji233: RegistryPath = %S\n"), __LINE__, RegistryPath->Buffer);
    target = MmCopyMemory;
    kprintf(("Line %d:xia0ji233: MmCopyMemory=%p\n"), __LINE__, target);
    g_Object = DriverObject;
    if (target) {
      for (int i = 0; i < sizeof(oldcode); i++) {
            oldcode = target;
      }
      *(UINT64*)(newcode + 2) = myMmCopyMemory;
      Hook();
    }
    else {
      kprintf(("xia0ji233:hahaha"));
    }
    return 0;
}
```

结果直接没有调用过。后面还同样的方法 hook 了其它的 API,诸如 KeStackAttachProcess 的,同样没有调用,于是陷入沉思,既然它没有把 token 写到 r3 那大概率在 r0 层。

同时在一次巧合中(开了 DbgView 的 verbose),发现 token 直接打印出来了。



那么 token2 就出来了 `token2: 8b3f14a24d64f3e697957c252e3a5686`

所以 flag 就是 `flag{757F4749AEBB1891EF5AC2A9B5439CEA-8b3f14a24d64f3e697957c252e3a5686}`

#### 编写程序,运行时修改尽量少的内存,让两段token输出成功。(满分2分)

首先看看内核层的输出吧,因为它本来就可以输出,直接调用的 DbgPrintEx 函数,只不过某个 Level 无法正常被接受罢了,尝试 hook 一下,看看 Level

```C
#include <ntifs.h>
#include <ntdef.h>
#include <ntstatus.h>
#include <ntddk.h>
#define MAX_BACKTRACE_DEPTH 20
#define SYMBOL L"\\??\\xia0ji2333"
#define kprintf(format, ...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, format, ##__VA_ARGS__)

UINT64 BaseAddr=NULL, DLLSize=0;


void DeleteDevice(PDRIVER_OBJECT pDriver) {
    kprintf(("Line %d:xia0ji233: start delete device\n"), __LINE__);
    if (pDriver->DeviceObject) {
      UNICODE_STRING Sym;
      RtlInitUnicodeString(&Sym, SYMBOL);//CreateFile
      kprintf(("Line %d:xia0ji233: Delete Symbol\n"), __LINE__);
      IoDeleteSymbolicLink(&Sym);
      kprintf(("Line %d:xia0ji233: Delete Device\n"), __LINE__);
      IoDeleteDevice(pDriver->DeviceObject);
    }
    kprintf(("Line %d:xia0ji233: end delete device\n"), __LINE__);
}



char newcode[] = {
    0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//mov rax,xxx
    0xFF,0xE0//jmp rax
};
char oldcode[] = {
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,
};

char *target;

KIRQL WPOFFx64()
{
    KIRQL irql = KeRaiseIrqlToDpcLevel();
    UINT64 cr0 = __readcr0();
    cr0 &= 0xfffffffffffeffff;
    __writecr0(cr0);
    _disable();
    return irql;
}

void WPONx64(KIRQL irql)
{
    UINT64 cr0 = __readcr0();
    cr0 |= 0x10000;
    _enable();
    __writecr0(cr0);
    KeLowerIrql(irql);
}

NTSTATUS Unhook() {
    KIRQL irql = WPOFFx64();
    for (int i = 0; i < sizeof(newcode); i++) {
      target = oldcode;
    }
    WPONx64(irql);
    return STATUS_SUCCESS;
}

NTSTATUS Hook() {
    KIRQL irql = WPOFFx64();
    for (int i = 0; i < sizeof(newcode); i++) {
      target = newcode;
    }
    WPONx64(irql);
    return STATUS_SUCCESS;
}


PDRIVER_OBJECT g_Object = NULL;
typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;
    PVOID EntryPoint;//驱动的进入点 DriverEntry
    ULONG SizeOfImage;
    UNICODE_STRING FullDllName;//驱动的满路径
    UNICODE_STRING BaseDllName;//不带路径的驱动名字
    ULONG Flags;
    USHORT LoadCount;
    USHORT TlsIndex;
    union {
      LIST_ENTRY HashLinks;
      struct {
            PVOID SectionPointer;
            ULONG CheckSum;
      };
    };
    union {
      struct {
            ULONG TimeDateStamp;
      };
      struct {
            PVOID LoadedImports;
      };
    };
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

VOID bianliqudongmokuai(PUNICODE_STRING name, UINT64* pBaseAddr,UINT64* pSize)
{
    LDR_DATA_TABLE_ENTRY *TE, *Tmp;
    TE = (LDR_DATA_TABLE_ENTRY*)g_Object->DriverSection;
    PLIST_ENTRY LinkList;
    ;
    int i = 0;
    LinkList = TE->InLoadOrderLinks.Flink;
    while (LinkList != &TE->InLoadOrderLinks)
    {
      Tmp = (LDR_DATA_TABLE_ENTRY*)LinkList;
      if (RtlCompareUnicodeString(&Tmp->BaseDllName, name, FALSE))
      {
      }
      else
      {
            kprintf(("Found Module!\n"));
            *pBaseAddr = (UINT64)(Tmp->DllBase);
            *pSize = (UINT64)(Tmp->SizeOfImage);
      }
      LinkList = LinkList->Flink;
      i++;
    }


}

typedef ULONG(*FuncPtr) (ULONG ComponentId,ULONG Level, PCSTR Format, ...);

ULONGmyDbgPrintEx( ULONG ComponentId,ULONG Level,PCSTR Format, ... ) {



    Unhook();
    FuncPtr func = (FuncPtr)target;
    kprintf(("call DbgPrintEx(%p,%p,%p)"), ComponentId, Level, Format);
    DbgBreakPoint();
    NTSTATUS s = func(ComponentId,Level,Format);
   
    Hook();

    return s;
}

void DriverUnload(PDRIVER_OBJECT pDriver) {
    kprintf(("Line %d:xia0ji233: start unload\n"), __LINE__);
    Unhook();
    DeleteDevice(pDriver);
}


NTSTATUS DriverEntry(
    _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath
) {
    DriverObject->DriverUnload = DriverUnload;
    kprintf(("Line %d:xia0ji233: RegistryPath = %S\n"), __LINE__, RegistryPath->Buffer);
    target = DbgPrintEx;
    kprintf(("Line %d:xia0ji233: DbgPrintEx=%p\n"), __LINE__, target);
    g_Object = DriverObject;
    if (target) {
      for (int i = 0; i < sizeof(oldcode); i++) {
            oldcode = target;
      }
      *(UINT64*)(newcode + 2) = myDbgPrintEx;
      Hook();
    }
    else {
      kprintf(("xia0ji233:hahaha"));
    }
    return 0;
}
```

持续输出了 `call DbgPrintEx(0000000000000000,0000000000000005,FFFFF1001067EB90)` Level=5几乎不能输出任何内容了,因此尝试hook替换让它可以输出,但是这里 Hook 还是太麻烦了,于是我选择打下断点之后栈回溯一下看看情况



可以发现关键 call 之前,有对 edx 赋值为 5,那么直接修改这个指令,把 hook 解掉看看能否输出。

这里我选择手动改一下,一共发现了三个位置,特征都是差不多的,都把 `mov edx,5` 改成 `mov edx,0`。



发现改完之后 token 成功输出了。

但是这里应该是驱动加载时候分配的内存写入的 shellcode,如果能知道地址,那么能去改掉这些指令,但是地址是我通过栈回溯找到的,如果没有hook掉系统 API,那么我根本不太可能去获取到shellcode的地址。于是想到它既然是不停地在打印的,必然创建了一个内核线程,那么我先遍历一下内核线程。

```C
#include <ntddk.h>
#define kprintf(format, ...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, format, ##__VA_ARGS__)
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath);
VOID UnloadDriver(PDRIVER_OBJECT DriverObject);
NTSTATUS EnumerateKernelThreads();

typedef NTSTATUS (*ZWQUERYSYSTEMINFORMATION)(ULONG, PVOID, ULONG, PULONG);
typedef struct _SYSTEM_PROCESS_INFORMATION {
    ULONG NextEntryOffset;
    ULONG NumberOfThreads;
    LARGE_INTEGER Reserved;
    LARGE_INTEGER CreateTime;
    LARGE_INTEGER UserTime;
    LARGE_INTEGER KernelTime;
    UNICODE_STRING ImageName;
    ULONG BasePriority;
    HANDLE ProcessId;
    HANDLE InheritedFromProcessId;
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
typedef struct _SYSTEM_THREAD_INFORMATION {
    LARGE_INTEGER KernelTime;
    LARGE_INTEGER UserTime;
    LARGE_INTEGER CreateTime;
    ULONG WaitTime;
    PVOID StartAddress;
    CLIENT_ID ClientId;
    ULONG Priority;
    LONG BasePriority;
    ULONG ContextSwitchCount;
    LONG State;
    LONG WaitReason;
} SYSTEM_THREAD_INFORMATION, *PSYSTEM_THREAD_INFORMATION;
typedef enum _SYSTEM_INFORMATION_CLASS {
    SystemProcessInformation = 5
} SYSTEM_INFORMATION_CLASS;
#define SystemModuleInformation 11

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    UNREFERENCED_PARAMETER(RegistryPath);
    DriverObject->DriverUnload = UnloadDriver;
    kprintf(("Driver Loaded\n"));
    return EnumerateKernelThreads();
}

VOID UnloadDriver(PDRIVER_OBJECT DriverObject) {
    UNREFERENCED_PARAMETER(DriverObject);
    kprintf(("Driver Unloaded\n"));
}

NTSTATUS EnumerateKernelThreads() {
    UNICODE_STRING routineName;
    RtlInitUnicodeString(&routineName, L"ZwQuerySystemInformation");
    ZWQUERYSYSTEMINFORMATION ZwQuerySystemInformation = (ZWQUERYSYSTEMINFORMATION)MmGetSystemRoutineAddress(&routineName);
    if (!ZwQuerySystemInformation) {
      return STATUS_UNSUCCESSFUL;
    }
    ULONG returnLength = 0;
    ZwQuerySystemInformation(SystemProcessInformation, NULL, 0, &returnLength);
    PVOID buffer = ExAllocatePool(NonPagedPool, returnLength);
    if (!buffer) {
      return STATUS_INSUFFICIENT_RESOURCES;
    }
    NTSTATUS status = ZwQuerySystemInformation(SystemProcessInformation, buffer, returnLength, &returnLength);
    if (!NT_SUCCESS(status)) {
      ExFreePool(buffer);
      return status;
    }
    PSYSTEM_PROCESS_INFORMATION current = (PSYSTEM_PROCESS_INFORMATION)buffer;
    while (TRUE) {
      PSYSTEM_THREAD_INFORMATION threadInfo = (PSYSTEM_THREAD_INFORMATION)(current + 1);
      for (ULONG i = 0; i < current->NumberOfThreads; i++) {

            if (((UINT64)(threadInfo->StartAddress) & 0xFFFF000000000000) == 0xFFFF000000000000) {
               kprintf(("Thread StartAddress: %p\n"), threadInfo->StartAddress);
            }
            
            threadInfo++;
      }

      if (current->NextEntryOffset == 0)
            break;
      
      current = (PSYSTEM_PROCESS_INFORMATION)((PUCHAR)current + current->NextEntryOffset);
    }

    ExFreePool(buffer);
    return STATUS_SUCCESS;
}
```

在中间我判断了一下地址是否为 `0xFFFF` 开头来判断是否为内核线程,然后打印出来之后,搜索通过栈回溯得到的 shellcode 的前几位。



很幸运地只能找到一个,多次实验之后发现它shellcode是不会变的(至少头几个字节),那么就可以匹配特征码去判断 shellcode 的地址。

并且可以手动计算一下 shellcode StartAddress 和 对应要修改的指令的偏移。

启动地址:`0xFFFFBB0EDB013DB0`

修改地址1:`0xffffbb0edb013e01`

修改地址2:`0xffffbb0edb013e64`

修改地址3:`0xffffbb0edb013ed4`

分别是 `shellcode+0x51+1`,`shellcode+0xb4+1`,`shellcode+0x124+1` 的位置。(+1是因为要改的操作数在指令的偏移处)

先编写一个遍历内核线程的程序,然后去特判它的特征码,来确定shellcode位置,最后再写入三个指令即可。

```C
#include <ntifs.h>
#include <ntdef.h>
#include <ntstatus.h>
#include <ntddk.h>

#define kprintf(format, ...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, format, ##__VA_ARGS__)
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath);
VOID UnloadDriver(PDRIVER_OBJECT DriverObject);
NTSTATUS EnumerateKernelThreads();

typedef NTSTATUS (*ZWQUERYSYSTEMINFORMATION)(ULONG, PVOID, ULONG, PULONG);
typedef struct _SYSTEM_PROCESS_INFORMATION {
    ULONG NextEntryOffset;
    ULONG NumberOfThreads;
    LARGE_INTEGER Reserved;
    LARGE_INTEGER CreateTime;
    LARGE_INTEGER UserTime;
    LARGE_INTEGER KernelTime;
    UNICODE_STRING ImageName;
    ULONG BasePriority;
    HANDLE ProcessId;
    HANDLE InheritedFromProcessId;
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
typedef struct _SYSTEM_THREAD_INFORMATION {
    LARGE_INTEGER KernelTime;
    LARGE_INTEGER UserTime;
    LARGE_INTEGER CreateTime;
    ULONG WaitTime;
    PVOID StartAddress;
    CLIENT_ID ClientId;
    ULONG Priority;
    LONG BasePriority;
    ULONG ContextSwitchCount;
    LONG State;
    LONG WaitReason;
} SYSTEM_THREAD_INFORMATION, *PSYSTEM_THREAD_INFORMATION;
typedef enum _SYSTEM_INFORMATION_CLASS {
    SystemProcessInformation = 5
} SYSTEM_INFORMATION_CLASS;
#define SystemModuleInformation 11

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    UNREFERENCED_PARAMETER(RegistryPath);
    DriverObject->DriverUnload = UnloadDriver;
    kprintf(("Driver Loaded\n"));
    return EnumerateKernelThreads();
}

VOID UnloadDriver(PDRIVER_OBJECT DriverObject) {
    UNREFERENCED_PARAMETER(DriverObject);
    kprintf(("Driver Unloaded\n"));
}

char CODE[]={
    0x48 ,0x8B ,0xC4 ,0x48 ,0x89 ,0x58 ,0x08 ,0x48 ,0x89 ,0x78 ,0x18 ,0x4C ,0x89 ,0x70 ,0x20 ,0x55,
    //0x48 ,0x8D ,0x68 ,0xA1 ,0x48 ,0x81 ,0xEC ,0xA0 ,0x00 ,0x00 ,0x00 ,0x48 ,0xBF ,0x4E ,0x93 ,0x32,
};

KIRQL WPOFFx64()
{
    KIRQL irql = KeRaiseIrqlToDpcLevel();
    UINT64 cr0 = __readcr0();
    cr0 &= 0xfffffffffffeffff;
    __writecr0(cr0);
    _disable();
    return irql;
}

void WPONx64(KIRQL irql)
{
    UINT64 cr0 = __readcr0();
    cr0 |= 0x10000;
    _enable();
    __writecr0(cr0);
    KeLowerIrql(irql);
}

MDLWriteMemory(PVOID pBaseAddress, PVOID pWriteData, SIZE_T writeDataSize)
{
    PMDL pMdl = NULL;
    PVOID pNewAddress = NULL;
    pMdl = MmCreateMdl(NULL, pBaseAddress, writeDataSize);
    if (NULL == pMdl)
    {
      return FALSE;
    }
    MmBuildMdlForNonPagedPool(pMdl);
    pNewAddress = MmMapLockedPages(pMdl, KernelMode);
    if (NULL == pNewAddress)
    {
      IoFreeMdl(pMdl);
    }
    RtlCopyMemory(pNewAddress, pWriteData, writeDataSize);
    MmUnmapLockedPages(pNewAddress, pMdl);
    IoFreeMdl(pMdl);
    return TRUE;
}

NTSTATUS EnumerateKernelThreads() {
    UNICODE_STRING routineName;
    RtlInitUnicodeString(&routineName, L"ZwQuerySystemInformation");
    ZWQUERYSYSTEMINFORMATION ZwQuerySystemInformation = (ZWQUERYSYSTEMINFORMATION)MmGetSystemRoutineAddress(&routineName);
    if (!ZwQuerySystemInformation) {
      return STATUS_UNSUCCESSFUL;
    }
    ULONG returnLength = 0;
    ZwQuerySystemInformation(SystemProcessInformation, NULL, 0, &returnLength);
    PVOID buffer = ExAllocatePool(NonPagedPool, returnLength);
    if (!buffer) {
      return STATUS_INSUFFICIENT_RESOURCES;
    }
    NTSTATUS status = ZwQuerySystemInformation(SystemProcessInformation, buffer, returnLength, &returnLength);
    if (!NT_SUCCESS(status)) {
      ExFreePool(buffer);
      return status;
    }
    for (int k = 0; k < 1; k++) {
      PSYSTEM_PROCESS_INFORMATION current = (PSYSTEM_PROCESS_INFORMATION)buffer;
      while (TRUE) {
            PSYSTEM_THREAD_INFORMATION threadInfo = (PSYSTEM_THREAD_INFORMATION)(current + 1);
            for (ULONG i = 0; i < current->NumberOfThreads; i++) {

                if (((UINT64)(threadInfo->StartAddress) & 0xFFFFBB0000000000) == 0xFFFFBB0000000000) {
                  kprintf(("StartAddress %p\n"), threadInfo->StartAddress);
               
                  if (MmIsAddressValid(threadInfo->StartAddress) && RtlEqualMemory(threadInfo->StartAddress, CODE, sizeof(CODE))) {
                        kprintf(("Shellcode Found in %p\n"), threadInfo->StartAddress);
                        char* shellcode = threadInfo->StartAddress;
                        MDLWriteMemory(shellcode + 0x51 + 1, "\x00", 1);
                        MDLWriteMemory(shellcode + 0xb4 + 1, "\x00", 1);
                        MDLWriteMemory(shellcode + 0x124 + 1, "\x00", 1);
                  }
                }
                threadInfo++;
            }

            if (current->NextEntryOffset == 0)
                break;
      
            current = (PSYSTEM_PROCESS_INFORMATION)((PUCHAR)current + current->NextEntryOffset);
      }
    }

    ExFreePool(buffer);
    return STATUS_SUCCESS;
}

```

> 该文件编译产物为 XSafe2.sys,运行题目之后加载这个驱动可以让 token2 输出。

通过下图可以看到,驱动加载后搜索到了 shellcode 的地址,并通过修改内存让 token成功输出了,但是自己做的时候发现是有概率的,有时候可能搜不到这个线程,从截图可以看出反复加载了4次才成功找到 shellcode,但是注入成功之后也成功输出了 token2。



token1 的话可以采用新建 `C:\2024GameSafeRace.token1` 文件的方式让它将 token1 打印出来,究其原因没有成功输出出来是因为创建文件的时候没有让它在文件不存在时创建。

后面发现写文件的进程好像是 TaskMgr,并且 token1 是可以独立运行的,所以可以把虚拟机的测试模式关了,后面只需要分析这个三环程序即可。

用调试器附加,虽然外挂程序检测了调试器,但是通过改名可以绕过,断在创建文件的API上 `CreateFileA`



栈回溯一下发现创建是失败的(返回-1)。



要让它成功输出让它创建成功即可,同时观察栈我注意到了有个参数3,而参数通过查阅 CreateFileA 的参数说明可知

```C
#define CREATE_NEW          1
#define CREATE_ALWAYS       2
#define OPEN_EXISTING       3
#define OPEN_ALWAYS         4
#define TRUNCATE_EXISTING   5
```

这也就解释了为什么创建一个文件能够成功写入,尝试把它修改成 1。



发现成功返回了,所以目的非常明确了,让它传的第五个参数改成1即可成功输出到文件中,而刚好注意到上面的一条指令有直接 mov xxx,3 的,把它改成1试试。



文件即使不存在也会创建并输出成功。

那么这里说一个可行的思路:同样注入一个 dll 给它,然后去遍历线程找到 shellcode 入口,计算偏移改掉这个指令,让 token3 成功输出。

先手动操作一遍:



此时可以发现线程入口是 0000019B070AAB48,要修改的指令地址为 0000019B070A4D6D

在 StartRoutine - 0x5ddb 的位置上,而且它的地址很明显,只要在堆上就符合条件,但是为了保险还是取一定长度的特征码去比较。

那么据此写一个针对 Taskmgr 的注入器(二进制文件为 Xinject.exe):

```cpp
#include<windows.h>
#include<iostream>
#include<time.h>
#include<stdlib.h>
#include<TlHelp32.h>
#define EXEFILEW L"Taskmgr.exe"
#define EXEFILE "Taskmgr.exe"
DWORD old;
SIZE_T written;
DWORD FindProcess() {
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 pe32;
    pe32 = { sizeof(pe32) };
    BOOL ret = Process32First(hSnap, &pe32);
    while (ret)
    {
      if (!wcsncmp(pe32.szExeFile,EXEFILEW, lstrlen(EXEFILEW))) {
            printf("找到程序 %s ,PID=%d\n", EXEFILE, pe32.th32ProcessID);
            return pe32.th32ProcessID;
      }
      ret = Process32Next(hSnap, &pe32);
    }
    return 0;
}
void InjectModule(DWORD ProcessId, const char* szPath)
{
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
    printf("进程句柄:%p\n", hProcess);
    LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    SIZE_T dwWriteLength = 0;
    WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength);
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL);
    WaitForSingleObject(hThread, -1);
    VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
    CloseHandle(hProcess);
    CloseHandle(hThread);
}
int main() {
    DWORD ProcessId = FindProcess();
    while (!ProcessId) {
      printf("未找到%s程序,等待两秒中再试\n",EXEFILE);
      Sleep(2000);
      ProcessId = FindProcess();
    }
    InjectModule(ProcessId, "Xhack.dll");
}
```

然后根据以上分析结果写一个改变这个代码的 DLL(二进制文件为 XHack.dll):

```cpp
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include<Windows.h>
#include<stdio.h>
#include<TlHelp32.h>

typedef enum _THREADINFOCLASS{
    ThreadBasicInformation,
    ThreadTimes,
    ThreadPriority,
    ThreadBasePriority,
    ThreadAffinityMask,
    ThreadImpersonationToken,
    ThreadDescriptorTableEntry,
    ThreadEnableAlignmentFaultFixup,
    ThreadEventPair_Reusable,
    ThreadQuerySetWin32StartAddress,
    ThreadZeroTlsCell,
    ThreadPerformanceCount,
    ThreadAmILastThread,
    ThreadIdealProcessor,
    ThreadPriorityBoost,
    ThreadSetTlsArrayAddress,
    ThreadIsIoPending,
    ThreadHideFromDebugger,
    ThreadBreakOnTermination,
    MaxThreadInfoClass
}THREADINFOCLASS;
typedef struct _CLIENT_ID{
    HANDLE UniqueProcess;
    HANDLE UniqueThread;
}CLIENT_ID;
typedef struct _THREAD_BASIC_INFORMATION{
    LONG ExitStatus;
    PVOID TebBaseAddress;
    CLIENT_ID ClientId;
    LONG AffinityMask;
    LONG Priority;
    LONG BasePriority;
}THREAD_BASIC_INFORMATION,*PTHREAD_BASIC_INFORMATION;
extern "C" LONG (__stdcall *ZwQueryInformationThread)(
    IN HANDLE ThreadHandle,
    IN THREADINFOCLASS ThreadInformationClass,
    OUT PVOID ThreadInformation,
    IN ULONG ThreadInformationLength,
    OUT PULONG ReturnLength OPTIONAL
    ) = NULL;

BYTE CODE[] = {
    0x40,0x53,0x48,0x83,0xEC,0x20,0x48,0x8B,0xD9,0x48,0x85,0xC9
};
BOOL hack() {
   

    HANDLE hThreadSnap = INVALID_HANDLE_VALUE;
    THREADENTRY32 te32;
    DWORD dwOwnerPID = GetCurrentProcessId();
    hThreadSnap = CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD, 0 );
    if( hThreadSnap == INVALID_HANDLE_VALUE )
    return( FALSE );
   
    te32.dwSize = sizeof(THREADENTRY32 );
    if( !Thread32First( hThreadSnap, &te32 ) )
    {
      CloseHandle( hThreadSnap );   // Must clean up the snapshot object!
      return( FALSE );
    }
    ULONG64 StartAddress;
    DWORD dwReturnLength;
   
   
   
    do
    {
      if( te32.th32OwnerProcessID == dwOwnerPID )
      {
            
            if (te32.th32OwnerProcessID) {
                HANDLE hThread = OpenThread(THREAD_QUERY_INFORMATION, FALSE, te32.th32ThreadID);
                if (hThread != NULL) {
                  

                  HINSTANCE hNTDLL = GetModuleHandle(L"ntdll");
                  (FARPROC&)ZwQueryInformationThread= ::GetProcAddress(hNTDLL,"ZwQueryInformationThread");
                  PVOID startaddr;                                                // 用来接收线程入口地址
                                        ZwQueryInformationThread(
                                                hThread,                                                      // 线程句柄
                                                ThreadQuerySetWin32StartAddress,      // 线程信息类型,ThreadQuerySetWin32StartAddress :线程入口地址
                                                &startaddr,                                                      // 指向缓冲区的指针
                                                sizeof(startaddr),                                        // 缓冲区的大小
                                                NULL                                                               
                                                );


                  if (!memcmp(startaddr,CODE,sizeof(CODE))) {
                        char msg;
                        sprintf(msg, "Found the Shellcode in address:%p", startaddr);
                        MessageBoxA(NULL, msg, "Success", MB_OK);
                        sprintf(msg, "Replace The byte %02x to 0x01 in addr %p\n",*((BYTE*)startaddr - 0x5ddb + 4) ,(BYTE*)startaddr - 0x5ddb + 4);
                        MessageBoxA(NULL, msg, "Success", MB_OK);
                        *((BYTE*)startaddr - 0x5ddb + 4) = 1;
                        return TRUE;
                  }
                  CloseHandle(hThread);
                }
            }
      }
    } while( Thread32Next(hThreadSnap, &te32 ) );


    //Don't forget to clean up the snapshot object.
    CloseHandle( hThreadSnap );
    return( FALSE );


}
BOOL APIENTRY DllMain( HMODULE hModule,
                     DWORDul_reason_for_call,
                     LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:

      if (!hack()) {
            MessageBoxA(NULL, "Fail", "FAIL", MB_OK);
      }
      else {
            MessageBoxA(NULL, "Success", "Success", MB_OK);
      }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
      break;
    }
    return TRUE;
}


```

运行 hack.exe 之后,运行 Xinject.exe 即可达到如下效果:



注入结束之后可以看到马上创建了文件。



第二题就完整地实现了。

附件说明:

- Xsafe2.sys:加载用于实现 token2 的输出
- Xinject.exe:用于将下面模块注入 Taskmgr.exe
- Xhack2.dll(运行时应改名为 XHack.dll):用于修改代码实现 token1 的输出

#### (3)编写程序,运行时修改尽量少的内存,让shellcode 往自行指定的位置写入token1成功。(满分3分)

其实还是 hook 这个地方,改它的写入文件名即可,这里我又注意到



往下顺着看,它把RAX写到了 RBP+0x20 的位置,但是最后又对它写了第五个参数,因此基本可以认为这个指令是无用的,尝试去进程分配一块内存,在这个地方将 RCX 的值赋值给它即可达成任意位置的写入。

这个指令位置在 StartRoutine - 0x5ddb - 0x67 处。

用下面的代码编译出 `XHack.dll` (运行时需要改名为这个,用第二题的注入器,附件中的文件名为 XHack3.dll)

```cpp
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include<Windows.h>
#include<stdio.h>
#include<TlHelp32.h>
#define Dest "C:\\hijackedByXia0ji233.token1"
typedef enum _THREADINFOCLASS{
    ThreadBasicInformation,
    ThreadTimes,
    ThreadPriority,
    ThreadBasePriority,
    ThreadAffinityMask,
    ThreadImpersonationToken,
    ThreadDescriptorTableEntry,
    ThreadEnableAlignmentFaultFixup,
    ThreadEventPair_Reusable,
    ThreadQuerySetWin32StartAddress,
    ThreadZeroTlsCell,
    ThreadPerformanceCount,
    ThreadAmILastThread,
    ThreadIdealProcessor,
    ThreadPriorityBoost,
    ThreadSetTlsArrayAddress,
    ThreadIsIoPending,
    ThreadHideFromDebugger,
    ThreadBreakOnTermination,
    MaxThreadInfoClass
}THREADINFOCLASS;
typedef struct _CLIENT_ID{
    HANDLE UniqueProcess;
    HANDLE UniqueThread;
}CLIENT_ID;
typedef struct _THREAD_BASIC_INFORMATION{
    LONG ExitStatus;
    PVOID TebBaseAddress;
    CLIENT_ID ClientId;
    LONG AffinityMask;
    LONG Priority;
    LONG BasePriority;
}THREAD_BASIC_INFORMATION,*PTHREAD_BASIC_INFORMATION;
extern "C" LONG (__stdcall *ZwQueryInformationThread)(
    IN HANDLE ThreadHandle,
    IN THREADINFOCLASS ThreadInformationClass,
    OUT PVOID ThreadInformation,
    IN ULONG ThreadInformationLength,
    OUT PULONG ReturnLength OPTIONAL
    ) = NULL;

BYTE CODE[] = {
    0x40,0x53,0x48,0x83,0xEC,0x20,0x48,0x8B,0xD9,0x48,0x85,0xC9
};
BYTE HACKCODE[] = {
    0x48,0xB9,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00//mov RCX,xxx
};
BOOL hack() {
   

    HANDLE hThreadSnap = INVALID_HANDLE_VALUE;
    THREADENTRY32 te32;
    DWORD dwOwnerPID = GetCurrentProcessId();
    hThreadSnap = CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD, 0 );
    if( hThreadSnap == INVALID_HANDLE_VALUE )
    return( FALSE );
   
    te32.dwSize = sizeof(THREADENTRY32 );
    if( !Thread32First( hThreadSnap, &te32 ) )
    {
      CloseHandle( hThreadSnap );   // Must clean up the snapshot object!
      return( FALSE );
    }
    ULONG64 StartAddress;
    DWORD dwReturnLength;
   
   
   
    do
    {
      if( te32.th32OwnerProcessID == dwOwnerPID )
      {
            
            if (te32.th32OwnerProcessID) {
                HANDLE hThread = OpenThread(THREAD_QUERY_INFORMATION, FALSE, te32.th32ThreadID);
                if (hThread != NULL) {
                  

                  HINSTANCE hNTDLL = GetModuleHandle(L"ntdll");
                  (FARPROC&)ZwQueryInformationThread= ::GetProcAddress(hNTDLL,"ZwQueryInformationThread");
                  PVOID startaddr;                                                // 用来接收线程入口地址
                                        ZwQueryInformationThread(
                                                hThread,                                                      // 线程句柄
                                                ThreadQuerySetWin32StartAddress,      // 线程信息类型,ThreadQuerySetWin32StartAddress :线程入口地址
                                                &startaddr,                                                      // 指向缓冲区的指针
                                                sizeof(startaddr),                                        // 缓冲区的大小
                                                NULL                                                               
                                                );


                  if (!memcmp(startaddr,CODE,sizeof(CODE))) {
                        char msg;
                        sprintf(msg, "Found the Shellcode in address:%p", startaddr);
                        MessageBoxA(NULL, msg, "Success", MB_OK);
                        sprintf(msg, "Replace The byte %02x to 0x01 in addr %p\n",*((BYTE*)startaddr - 0x5ddb + 4) ,(BYTE*)startaddr - 0x5ddb + 4);
                        MessageBoxA(NULL, msg, "Success", MB_OK);
                        *((BYTE*)startaddr - 0x5ddb + 4) = 1;

                        LPVOID buffer=VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_READWRITE);
                        memcpy(buffer, Dest, sizeof(Dest));
                        *(ULONG64*)(HACKCODE + 2) = (ULONG64)buffer;

                        memcpy((BYTE*)startaddr - 0x5ddb - 0x67, HACKCODE, sizeof(HACKCODE));

                        MessageBoxA(NULL, "PATH replace to " Dest, "Success", MB_OK);
                        return TRUE;
                  }
                  CloseHandle(hThread);
                }
            }
      }
    } while( Thread32Next(hThreadSnap, &te32 ) );


    //Don't forget to clean up the snapshot object.
    CloseHandle( hThreadSnap );
    return( FALSE );


}
BOOL APIENTRY DllMain( HMODULE hModule,
                     DWORDul_reason_for_call,
                     LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:

      if (!hack()) {
            MessageBoxA(NULL, "Fail", "FAIL", MB_OK);
      }
      else {
            MessageBoxA(NULL, "Success", "Success", MB_OK);
      }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
      break;
    }
    return TRUE;
}


```

先分配一个内存,写入地址,再构造 `mov rcx,xxx` 指令,最后替换到指定位置即可。



内容也非常完美没有被改变



4,5如上所示,源代码均在本文中提供,在打包的可执行文件中:

总共的附件说明:

- XSafe2.sys:加载后输出token2
- Xinject.exe:注入任务管理器的注入器(2,3题通用)
- XHack2.dll:实现第二题的dll(运行时需改名为XHack.dll,且与注入器在同一目录下)
- XHack3.dll:实现第三题的dll(运行时需改名为XHack.dll,且与注入器在同一目录下)

## 花絮

三天没打满,中间一天抽空去拿了个ACM省赛。



然后第二天又接到调剂复试通知赶路去了。



不过最后好在还是赶着最后完成了赛题 233。

## 参考文献

https://www.ctyun.cn/zhishi/p-233867

Hmily 发表于 2024-4-15 12:33

{:1_907:}因为之前几年官方找我们合作,还提供板块引导分享技术,所以加了对应分类,这两年没有和我们合作,这个板块也就荒废了,所以分类也没追加,如果分享较多我们再加分类吧,或者最好直接发到脱壳破解区好了,这样展现更多。

大罗金仙 发表于 2024-4-15 13:00

前排看一下大佬的操作

fish820 发表于 2024-4-15 09:28

好厉害的大学生

aa702429162 发表于 2024-4-15 09:31

CTF大佬,解题思路学习了

猫携 发表于 2024-4-15 09:39

互相学习,共获提升

QQ40052090 发表于 2024-4-15 09:50

TX游戏有一个安全的?

cxx0515 发表于 2024-4-15 11:47

厉害,看的我晕头转向的,学习了

PJ997272250 发表于 2024-4-15 11:48


厉害,看的我晕头转向的,学习了

qwq23496 发表于 2024-4-15 12:23

支持一下
页: [1] 2 3 4 5 6 7 8 9
查看完整版本: 腾讯游戏安全大赛2024初赛题解