首页
社区
课程
招聘
29
[原创]某二次元开放世界冒险游戏反作弊分析报告
发表于: 2025-2-14 12:13 11377

[原创]某二次元开放世界冒险游戏反作弊分析报告

2025-2-14 12:13
11377

好久没碰某二次元开放世界冒险游戏了,听说新升级了反作弊,故来一探究竟,并尝试实现一些简单的功能。

基本保护分析

这种级别的游戏首先不考虑静态分析,直接跑起来。不出意外肯定不能直接内存读写,想附加调试器也是附加不上的,所以选择先从驱动入手,游戏加载时会加载驱动。

先尝试简单的拦截,方法很多:注册 LoadImage 回调拦截,改驱动名等等等。后者比较好实现,但是运行游戏一段时间会弹窗强制退出。

而如果说让保护加载,自己起一个句柄提权的驱动,则会被弹窗退出。

尝试过在虚拟机里直接启动游戏,不出意外也是弹窗。

使用启动时注入的方式,手动 Create 进程挂起,再远线程注入,可以将 DLL 注入,因为游戏刚运行的时候是没有驱动保护的,自然可以获得正常的游戏句柄。

注入功能测试

DLL 直接用 imgui 做 hook 就行,网上框架巨多,先浅浅尝试一下改锁帧的功能,由于这个游戏锁 60 帧,因此玩的很难受,尝试找一下这个值。

反复修改反复找可以找到四个值,地址较小的那个是真实值

imgui里面直接用这个值绑定滑动条,实现帧率解锁。

R3分析

面临的难点主要是反调试和反虚拟机。

反虚拟机

先说结论:R3程序使用了多种类型的反虚拟机技术,大部分通过hook api 的形式可以直接过掉。

  • 虚拟机设备检测——Hook CreateFileA 和 CreateFileW 拦截常见的虚拟设备
  • 虚拟机系统文件检测(sys和dll)——Hook CreateFileA 和 CreateFileW 虚拟机的 sys 和 dll 文件
  • 进程检测——Hook ProcessNextW 跳过虚拟机中才会存在的进程
  • 驱动目录检测——Hook NtQueryDirectory 拦截虚拟机中的驱动服务,改成其它任意名字即可
  • 计时器检测——Hook GetTickCount 在监测点修改返回值降低时间间隔
  • MAC地址检测——Hook GetAdaptersInfo 将MAC地址的厂商号替换为非虚拟机厂商的厂商号
  • 注册表检测——暂时是配合 sys 文件一起做检测的,可以不用拦截,实际上也可以 Hook OpenKey 之类的注册表函数
  • 模块检测——Hook ModuleNextW 跳过虚拟机相关模块

虚拟设备检测

Hook CreateFileWCreateFileA 这两个 API,可以看出在尝试打开如下的设备和文件

1
2
3
4
\\.\vmmemctl
C:\Windows\system32\DRIVERS\vm3dmp.sys
C:\Windows\system32\drivers\vm3dmp_loader.sys
...

不用想,游戏打开这些文件肯定是在检测虚拟机,这里将文件添加到一个 set 中,每次打开遍历一遍,遇到它检测的文件就直接返回无效句柄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
HANDLE gh_CreateFileW(...) {
    for (auto it : DeviceFileBlacklist) {
        if (CaseInsensitiveContains(lpFileName, it)) {
            DBG_PRINT("black device \"%ws\" not allowed to open\n", lpFileName);
            return INVALID_HANDLE_VALUE;
        }
    }
    HANDLE hFile = CreateFileW(...);
    bool flag = true;
    for(auto it:FileBlacklist){
        if (CaseInsensitiveContains(lpFileName,it)) {
            flag = false;
            break;
        }
    }
    DBG_PRINT("CreateFileW called with %ws return value %p\n", lpFileName, hFile);
    return hFile;
}

只需要对 yxxxshen.exemxxxbase.dll 两个模块做 IAT hook 即可。下面是拦截成功的一些日志,实际上还有更多的设备,这里不一一展示:

1
2
3
4
[Debug Info]black device "\\.\vmmemctl" not allowed to open
[Debug Info]black device "C:\Windows\system32\DRIVERS\vm3dmp.sys" not allowed to open
[Debug Info]black device "C:\Windows\system32\drivers\vm3dmp_loader.sys" not allowed to open
...

进程检测

运行过程中会有一段调用了进程遍历的关键函数 Process32NextW,应该是检测虚拟机的相关进程,这里直接匹配当前虚拟机存在的一些虚拟机特有的进程不让它返回即可。

1
2
3
4
5
6
7
8
9
10
11
12
BOOL gh_ProcessNextW(HANDLE hSnapshot, LPPROCESSENTRY32W lppe) {
    BOOL ret = Process32NextW(hSnapshot, lppe);
    WCHAR *szExeFile = lppe->szExeFile;
    while (CaseInsensitiveContains(szExeFile, L"vm")||CaseInsensitiveContains(szExeFile,L"VGAuthService") && ret) {
        DBG_PRINT("Found Vm in Process name %ws,try to execute again\n", szExeFile);
        ret = Process32NextW(hSnapshot, lppe);
        szExeFile = lppe->szExeFile;
        DBG_PRINT("new Process Name %ws pid=%d ret=%d\n", lppe->szExeFile, lppe->th32ProcessID, ret);
    }
    DBG_PRINT("ProcessNextW called with %ws pid=%d ret=%d\n", lppe->szExeFile,lppe->th32ProcessID ,ret);
    return ret;
}

如果找到 vm 相关进程则持续调用,直到进程名不包含 vm 或者为 VGAuthService 即可。下面是一些拦截成功的日志:

1
2
3
4
5
[Debug Info]Found Vm in Process name vm3dservice.exe,try to execute again
[Debug Info]new Process Name vmtoolsd.exe pid=3916 ret=1
[Debug Info]Found Vm in Process name vmtoolsd.exe,try to execute again
[Debug Info]new Process Name svchost.exe pid=3928 ret=1
[Debug Info]ProcessNextW called with svchost.exe pid=3928 ret=1

驱动目录检测

游戏调用了 NtOpenDirectoryObjectNtQueryDirectoryObject 两个 API,经过测试发现它打开了 \Device 路径,也就是开始遍历了驱动对象。

这两个 api 可以先hook打印,但是单纯绕过检测 hook 后者即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NTSTATUS gh_NtQueryDirectoryObject(...) {
    auto ret = NtQueryDirectoryObject(...);
    auto info = (POBJECT_DIRECTORY_INFORMATION)Buffer;
    for(auto it:DeviceBlackList){
        if(CaseInsensitiveEqual(info->Name.Buffer,it)){
            DBG_PRINT("NtQueryDirectoryObject name=\"%wZ\" return %d Deny to open!\n",
            info->Name, info->TypeName, ret);
            info->Name = DeniedDevice;
            return 0;
        }
    }
    DBG_PRINT("NtQueryDirectoryObject name=\"%wZ\",Type=\"%wZ\" return %d\n",
    info->Name, info->TypeName, ret);
    return ret;
}

这里也给出一些拦截成功的日志

1
2
3
[Debug Info]NtQueryDirectoryObject name="gpuenergydrv",Type="Device" return 0
[Debug Info]NtQueryDirectoryObject name="VMCIHostDev" return 697297488 Deny to open!
[Debug Info]NtQueryDirectoryObject name="00000068",Type="Device" return 0

计时器检测

注意到 mxxxbase.dll 的一个函数

GetTickCount64 获取系统启动以来经过的毫秒数。

它做了 10 次测试,每次测试 10000 条 cpuid 指令运行所需的时间,在虚拟机里,它很大,物理机中几乎每次都为 0。

那么便可以:

强制将两次运行的 cpuid 的时间设为一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ULONGLONG st=40000;
 
ULONGLONG gh_GetTickCount64() {
    auto ret = GetTickCount64();
    if (st == 0) {
        DBG_PRINT("GetTickCount64 called %lld\n", ret);
        st = ret;
    }
    else {
        DBG_PRINT("GetTickCount64 called change %lld to %lld\n", ret, st);
        ret = st;
        st = 0;
    }
    return ret;
}

下面是日志

1
2
3
4
[Debug Info]GetTickCount64 called 4117687
[Debug Info]GetTickCount64 called change 4117718 to 4117687
[Debug Info]GetTickCount64 called 4117734
[Debug Info]GetTickCount64 called change 4117812 to 4117734

可以对比得到,hook 前和 hook 后的差距大概是有几十毫秒的,这里会被检测到,通常物理机的间隔都是 0。

MAC地址检测

该函数调用了,但是没进行检测,提前写好以免后面加这个检测,检测的方式通常是检查 MAC 地址前三字节的信息看厂商是否为 Vmware 之类的。

1
2
3
4
5
6
7
8
9
ULONG gh_GetAdaptersInfo(...) {
    auto ret = GetAdaptersInfo(AdapterInfo, SizePointer);
    DBG_PRINT("GetAdaptersInfo called with %p %p return %d\n",...);
    //换成intel的MAC地址60:45:2E
    AdapterInfo->Address[0] = 0x60;
    AdapterInfo->Address[1] = 0x45;
    AdapterInfo->Address[2] = 0x2E;
    return ret;
}

注册表检测

hook 注册表相关的 api,拦截对应 open 的 key 的名字,实际上也是有调用没检测。

这里输出了一些相关log

1
2
3
[Debug Info]RegOpenKeyExA called with FFFFFFFF80000002 "SYSTEM\CurrentControlSet\services\vm3dmp_loader" 0 131353 000000702B0FF5B0 return 0
[Debug Info]CreateFileW called with C:\Program Files (x86)\mihoyo\games\Genshin Impact Game\yuanshen_Data\Persistent\base_res_version_hash return value 0000000000000CDC
[Debug Info]black device "C:\Windows\system32\drivers\vm3dmp_loader.sys" not allowed to open

但是预计可能是两个一起检测的,即:注册表判断服务是否存在,再判断驱动文件是否存在,有一样不成立就不认为检测到了虚拟机。

最终效果

过完这些虚拟机检测之后,也是成功可以在虚拟机中启动 yxxxshen.exe 了。

反调试

R3的反调试相对比较简单,除了众所周知的 IsDebuggerPresent 之外,早期的版本似乎 hookDbgBreakPointDbgUiRemoteBreakin 两个 API 来防止调试器附加,现在仍有 hook,不过只 hookDbgBreak,并且同样也有 ThreadHideFromDebugger 检测。

  • IsDebuggerPresent:hook 返回 0 即可。
  • ThreadHideFromDebugger:需要根据参数和调用的时机合理地选择返回,稍有不慎就会crash,具体看下文分析。
  • API hook:目前无须绕过。

ThreadHideFromDebugger

NtSetInformationThread 这个 API 本意是设置线程优先级的,其中有一个参数 ThreadInformationClass,这是一个 THREADINFOCLASS 的枚举类型。

1
2
3
4
5
6
7
8
9
10
typedef enum _THREADINFOCLASS {
    ThreadBasicInformation          = 0,
    //...
    ThreadPriorityBoost             = 14,
    ThreadSetTlsArrayAddress        = 15,   // Obsolete
    ThreadIsIoPending               = 16,
    ThreadHideFromDebugger          = 17,
     //...
    MaxThreadInfoClass              = 51,
} THREADINFOCLASS;

其中注意到 0x11 即为 ThreadHideFromDebugger,字面意思也不难理解,就是从调试器中隐藏该线程,据看雪一篇文章的分析,该函数关于 ThreadHideFromDebugger 的实现如下

1
2
3
4
5
6
7
8
9
10
11
12
case ThreadHideFromDebugger:
    if (ThreadInformationLength != 0) {
        return STATUS_INFO_LENGTH_MISMATCH;
    }
    st = ObReferenceObjectByHandle (...);
    if (!NT_SUCCESS (st)) {
        return st;
    }
    PS_SET_BITS (&Thread->CrossThreadFlags, PS_CROSS_THREAD_FLAGS_HIDEFROMDBG);
    ObDereferenceObject (Thread);
    return st;
    break;

可以看出当 classThreadHideFromDebugger 时,若 ThreadInformationLength 不为 0 则返回一个错误。因此过这个反调试不能无脑拦截 classThreadHideFromDebugger 的调用,而应注意这里的 Length 是否为 0。根据拦截 yxxxshen.exe 的调用可以看出。

1
2
[Debug Info]NtSetInformationThread called with handle fffffffe 17 at ... length 1 return c0000004
[Debug Info]NtSetInformationThread called with handle fffffffe 17 at ... length 0 return 0

它连续调用了两次,第一次估计设置 Length 为 1,看是否调用失败,第二次才是真正的反调试,因此需要辨别出这一点。

似乎也不难写出它的 hook 函数?

1
2
3
4
5
6
7
8
9
10
UINT64 gh_NtSetInformationThread(...) {
    if(ThreadInformationClass==0x11 && ThreadInformationLength==0){
        DBG_PRINT("Try to set ThreadHideFromDebugger,Stop it\n");
        return 0;
    }
    auto ret = NtSetInformationThread(...);
    DBG_PRINT("lasterror=%d\n", GetLastError());
    DBG_PRINT("NtSetInformationThread called with handle %x %d at %p length %d return %x\n",...);
    return ret;
}

但是很不幸的是,你会得到一个闪退。

思路似乎中断了,于是考虑看看与之相近的 API,也就是 NtQueryInformationThread

1
2
3
4
5
6
[Debug Info]NtQueryInformationThread called with handle fffffffe 17 at ... length 4 return c0000004
[Debug Info]NtQueryInformationThread called with handle fffffffe 17 at ... length 1 return 0
[Debug Info]NtSetInformationThread called with handle fffffffe 17 at ... length 1 return c0000004
[Debug Info]NtSetInformationThread called with handle fffffffe 17 at ... length 0 return 0
[Debug Info]NtQueryInformationThread called with handle fffffffe 17 at ... length 4 return c0000004
[Debug Info]NtQueryInformationThread called with handle fffffffe 17 at ... length 1 return 0

可以看到在前后各成功调用一次 NtQueryInformationThread,并且将 class 设为了 ThreadHideFromDebugger

这不对吧,query 它能干什么呢,对了,查询信息,可能是需要查询跟隐藏线程调试器相关的字段,那么会不会是因为成功 set 了和没成功 set 了情况不太一样呢?

这里 hook 掉看看前后查询的数据的区别。

1
2
3
4
5
6
7
[Debug Info]past information=34
[Debug Info]after information=00
[Debug Info]NtQueryInformationThread called with handle fffffffe 17 at ... length 1 return 0
[Debug Info]NtSetInformationThread called with handle fffffffe 17 at ... length 0 return 0
[Debug Info]past information=95
[Debug Info]after information=01
[Debug Info]NtQueryInformationThread called with handle fffffffe 17 at ... length 1 return 0

这里我保留关键的 LOG,也可以看出来,它在 set 前后分别查询了一次,第一次查询得知的结果是 0,而成功调用 set 之后得到的结果会是 1,如果仅仅 hook set 不让它调用则会在第二次查询也得到 0 的结果,这便是之前闪退的原因了。

因此对于这个反调试,需要同时 hook NtQueryInformationThreadNtSetInformationThread,严格判断参数,并合理过滤掉一些检测反调试和反-反-反调试的东西。

IsDebuggerPresent

这个已经被玩烂了的 API 相信是第一个被考虑到的,hook它永远返回 0 就行了。

最终效果

可以在虚拟机中,附加调试器的情况下运行该二次元开放世界冒险游戏且不报错。

R0分析

主要尝试分析检测逻辑,尽可能地在不影响功能的情况下过掉检测。

反调试

先给结论:反调试主要由驱动创建的一个线程实现,入口点在 0x2f0c0,重复顺序执行以下逻辑:

  • 读取 KdDebuggerEnabled 标志位,如果置 1 则清零。
  • 找到寻找 kdcom.dll,使用 MDL 的方式将 kdcom.dlldata 段清零。
  • 读取 KdDebuggerNotPresent 标志位。
  • 读取 KdDebuggerEnabled 标志位。
  • 读取 KdDebuggerNotPresent 标志位。

下面给出笔者的分析步骤和对应的解决方案。

动态调试

R0 层的反调试其实反而没那么难,因为 API 就那么几个,HxxxKProtect.sys 的反调试具体表现为,在双机调试的情况下成功加载之后会导致调试器无响应。

根R0调试相关的API找一下即可,通过 IDA 直接搜索导入表或者字符串,得到以下几个跟调试器相关的

  • KdDebuggerNotPresent
  • KdDebuggerEnabled

根据查阅 MSDN 可知,这两个是内核中的标志位,尝试 hook 将它修改到其它位置。运行之后发现调试器依旧被剥离,但是虚拟机似乎也卡死,并没有蓝屏,在游戏终端中发现了上传日志。

路径中可以看到上传了由于驱动导致的蓝屏(dmp),和自己的信息文件。

info.txt 包含了操作系统的信息,硬件信息和uid信息。

1
2
3
4
5
6
7
8
9
10
11
12
version:5.2_rel CNRELWin5.2.0_28336591_29063028_28887986_28772242_28351161
deviceName:DESKTOP-DLBRLIS
time:2024-12-28 15.08.15.9001
deviceModel:VMware20,1 (VMware, Inc.)
operatingSystem:Windows 10  (10.0.19045) 64bit Microsoft Windows NT 10.0.19045.0
uid:14xxxxxx3
memoryInfo:695
cpuInfo:Intel(R) Core(TM) i9-14900HX
gpuInfo:VMware SVGA 3D
clientIp:fe80::374b:96c4:2526:ec61
isRelease:1
type:Windows Crash Release

这些信息大概率都是注册表或者一个 API GetSystemFirmwareTable 读出来的,这里为了防止被上传,最好把注册表处理干净,所有跟 Vmware 相关的全部替换掉。

其它特征去除直接用大表哥的 vmloader(大表哥nb),不知道这里怎么访问这两个标志的,所以先尝试 Hook MmGetSystemRoutineAddress,再去导入表替换两个标志位。

创建一个线程,持续输出两个标志位

1
2
3
4
5
6
7
8
VOID Routine() {
    while (TRUE) {
        DBG_PRINT("%d %d\n", *KdDebuggerEnabled, *KdDebuggerNotPresent);
        LARGE_INTEGER interval;
        interval.QuadPart = -10ll * 1000 * 1000;
        KeDelayExecutionThread(KernelMode, FALSE, &interval);
    }
}

附加调试器的情况下,输出应当是 1 0

加载游戏之后,会发现标志位变为了 0 1,而 KdDebuggerEnabled 标志位一旦被复位,windbg 会直接被剥离,因此需要阻止。

这里本想尝试加载驱动后,设置硬件断点在 KdDebuggerEnabled 字符串和对应的标志位中,但是似乎会有检测,如果设置了硬断驱动则会加载失败。

通过动调,还是找到了关键的指令。

1
2
.upx0:000000014034794F                 mov     rsi, [rcx]
.upx0:0000000140347952                 mov     [r11], rsi

[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!

最后于 2025-2-14 12:17 被xi@0ji233编辑 ,原因:
收藏
免费 29
支持
分享
赞赏记录
参与人
雪币
留言
时间
艾米哈柏
感谢你的积极参与,期待更多精彩内容!
3天前
thexin7
感谢你的积极参与,期待更多精彩内容!
2025-3-6 14:35
mb_shzsxtje
+6
感谢你分享这么好的资源!
2025-3-6 13:40
kishou_yusa
为你点赞!
2025-2-26 11:00
老小白
非常支持你的观点!
2025-2-25 10:41
mb_vjjsrsbp
你的帖子非常有用,感谢分享!
2025-2-24 13:36
powerpcer
这个讨论对我很有帮助,谢谢!
2025-2-24 07:32
mb_ytalqmdq
+1
你的分享对大家帮助很大,非常感谢!
2025-2-21 12:00
3zureus
你的分享对大家帮助很大,非常感谢!
2025-2-20 10:58
mb_sedplcgt
你的帖子非常有用,感谢分享!
2025-2-19 23:57
vyang2024
+1
你的帖子非常有用,感谢分享!
2025-2-19 23:05
hjf571x
+1
期待更多优质内容的分享,论坛有你更精彩!
2025-2-19 17:04
七夜大大
你的帖子非常有用,感谢分享!
2025-2-19 17:03
wuxiwudi
你的分享对大家帮助很大,非常感谢!
2025-2-19 14:02
PeterZheng
为你点赞!
2025-2-19 10:21
lracker
+2
为你点赞!
2025-2-18 22:44
ufufjvjvj
感谢你的积极参与,期待更多精彩内容!
2025-2-17 23:01
xiangyandt
+1
这个讨论对我很有帮助,谢谢!
2025-2-17 10:48
ifyou
你的帖子非常有用,感谢分享!
2025-2-17 10:07
Amun
+1
这个讨论对我很有帮助,谢谢!
2025-2-17 09:59
木志本柯
+5
你的帖子非常有用,感谢分享!
2025-2-16 17:21
chengdrgon
谢谢你的细致分析,受益匪浅!
2025-2-16 12:39
TubituX
+1
感谢你的贡献,论坛因你而更加精彩!
2025-2-16 01:21
P_Hac
+1
你的帖子非常有用,感谢分享!
2025-2-15 18:01
hahayzl
谢谢你的细致分析,受益匪浅!
2025-2-14 22:28
kekao
+1
这个讨论对我很有帮助,谢谢!
2025-2-14 22:25
moshuiD
谢谢你的细致分析,受益匪浅!
2025-2-14 19:35
令狐双
感谢你的贡献,论坛因你而更加精彩!
2025-2-14 16:02
Kvancy
+5
谢谢你的细致分析,受益匪浅!
2025-2-14 14:34
打赏 + 100.00雪花
打赏次数 1 雪花 + 100.00
收起 
赞赏  mb_rsjnrjwy   +100.00 2025/03/02 感谢分享~
最新回复 (14)
雪    币: 1036
活跃值: (1506)
能力值: ( LV9,RANK:404 )
在线值:
发帖
回帖
粉丝
2

是二次元黑客!

2025-2-14 12:21
0
雪    币: 1352
活跃值: (2411)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
3
精彩
2025-2-14 14:00
0
雪    币: 710
活跃值: (649)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
4
Hacker!!!
2025-2-14 14:29
0
雪    币: 2224
活跃值: (1376)
能力值: ( LV5,RANK:76 )
在线值:
发帖
回帖
粉丝
5
太厉害了师傅,向师傅学习!
2025-2-14 19:35
0
雪    币: 1843
活跃值: (1498)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
6
我去,是二次元黑客!
2025-2-14 20:23
0
雪    币: 3368
活跃值: (2236)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
MYGA
2025-2-14 21:26
0
雪    币: 9617
活跃值: (6860)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
8
原神启动!
2025-2-15 04:14
0
雪    币: 1434
活跃值: (1699)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
9
启动!
2025-2-15 16:13
0
雪    币: 4939
活跃值: (4842)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
666很棒的对抗过程,货真驾驶的逆向分析过程。
2025-2-16 17:21
0
雪    币: 2943
活跃值: (3400)
能力值: ( LV8,RANK:147 )
在线值:
发帖
回帖
粉丝
11
mark
2025-2-19 10:37
0
雪    币: 825
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
崇拜的眼神投来,跪拜
2025-2-19 17:05
0
雪    币: 6313
活跃值: (1066)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
13
感谢分享
2025-2-19 20:49
0
雪    币: 20
活跃值: (1024)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
你简直是牛逼给牛逼开门牛逼到家了
2025-2-24 05:07
0
雪    币: 39
活跃值: (414)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
mark
2025-3-2 11:49
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册