首页
社区
课程
招聘
[原创]Windows Kernel Programming 笔记 1~5 内核开发入门
发表于: 2022-1-18 17:24 34275

[原创]Windows Kernel Programming 笔记 1~5 内核开发入门

2022-1-18 17:24
34275

描述一些Windows内部工作中最重要、最基本的概念,部分概念将在后面的章节做更详细的研究

进程不运行(Processes dont't run - processes manage),线程才执行代码

进程拥有以下内容:

每个进程拥有自己的虚拟、私有、线性地址空间
(该地址空间初始时几乎为空,然后pe、ntdll.dll开始被影射,接着是其他子系统dll)

32位进程默认地址空间2GB,设置pe中的LARGEADDRESSAWARE标志可以增加到3GB(32位系统)或4GB(64位系统)

64位进程默认地址空间128TB(win8之前是8TB)

虚拟内存被映射到物理内存(RAM)或临时驻留在文件中(如page file)
如果不在物理内存,则触发page fault异常,并或取数据到物理内存中

页(page)是内存管理的单位,默认大小为4KB

虚拟内存中的页处于三种状态之一

系统空间与进程无关

系统空间就是内核

实际执行代码的是线程

线程拥有的最重要的内容:

线程最常处于的状态:

括号中的数字是状态号:

Running(2) => Waiting(5) => Deferred Ready(7), Ready(1) => Running(2)

线程至少有一个位于内核空间的栈(32位系统12KB,64位系统24KB)

用户态的线程还有一个位于所属进程空间的栈(默认上限1MB)

线程RunningReady时,内核栈驻留在RAM

栈初始时会尽可能少提交页(最少一页),剩下的页设置为Reserved,而最后一个Committed的页的下一页设置为PAGE_GUARD

原标题:System Services (a.k.a. System Calls)

R3代码通过系统调用完成一些只能在R0下完成的功能,如分配内存、打开文件、创建线程等

大致流程是:
调用subsystem dll(如kernel32.dll)中的文档化api(如CreateFile)
进入NTDLL中的 Native Api(如NtCreateFile)
进入内核中的系统服务分发函数
进入Native Api对应的内核中的函数

Native Api将调用号存入eax然后进入r0的系统服务分发函数,eax实际是SSDT(System Service Dispatch Table)的下标

对象被引用计数,当计数为0时才会被释放

句柄是进程的对象表的索引

注意:返回值为句柄的函数,大多数失败时返回0。有些返回INVALID_HANDLE_VALUE (-1),比如CreateFile

句柄值是4的倍数,0不是有效句柄值

某些类型的对象可以有名称,可用于通过合适的 Open 函数按名称打开对象。

用户模式调用 Create 函数按名称创建对象,如果存在,则仅打开现有对象。

winObj中显示的名称有时不是对象的真实名称:

Process Explorer 的句柄视图中的访问列显示用于打开或创建句柄的访问掩码

Process Explorer中显示的引用数(References)不是实际引用数(outstanding references)

[windbg]中用!trueref获取实际引用数(actual reference)

本章主要是关于准备内核开发所需的环境,包括开发和调试的工具以及环境配置

以及启动和运行内核驱动的知识

然后写一个可以加载和卸载的驱动

首先按 2.1安装工具 完成安装,然后为驱动开发配置虚拟机(未包括内核调试的配置)

安装无签名驱动

如果驱动没有签名,安装驱动需要以该模式启动系统

显示内核调试信息

HKLM\SYSTEM\CurrentControlSet\Control\Session Manager添加一个名为Debug Print Filter的键
在键中添加一个DWORD,名为DEFAULT,值为8

虚拟机文件共享

实际操作时,安装在虚拟机中(避免本机崩溃),需要共享项目的文件给虚拟机

共享本机的 vs解决方案文件夹 给虚拟机,名称为MyDriver

虚拟机中的debug输出路径为:\\vmware-host\Shared Folders\MyDriver\x64\Debug

驱动调试工具

安装完WDK后,把C:\Program Files (x86)\Windows Kits\10\Tools\x64这个目录复制到虚拟机中,这个是x64下的驱动开发调试工具,比如用于查看内存池的poolmon

需要vs2019、windows 10 sdk(vs2019中安装)、windows 10 driver kit(WDK)

以及 Sysinternals,该工具包含debug view、process monitor等一系列有用的工具

在实际编译中发现,新版本的vs驱动项目默认开启缓解Spectre 漏洞

可以在c/c++、代码生成中关闭该项,或在vs installer中安装对应工具

vs2019中选择创建一个Empty WDM Driver,创建完成后有个inf后缀的文件,暂时不需要,删除掉

DriverEntry 是驱动的默认入口点

系统线程以IRQL_PASSIVE_LEVEL(0)调用 DriverEntry

DriverEntry函数原型:

一个简单的驱动(sample.cpp):

安装驱动和安装用户态服务相似,需要调用Create Service API或使用工具

sc.exe(系统自带)是著名工具之一

安装驱动需要管理员权限

创建服务项:

随后就能在注册表(regedit.exe)的HKLM\System\CurrentControlSet\Services\Sample中看到该项

注册表项位置:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Sample

假设binPath= c:\,注册表项ImagePath= \??\c:\

假设binPaht= "\\vmware-hots\",注册表项ImagePath= \??\UNC\vmware-hots\

加载驱动(启动服务):

在process explorer中,选择System进程,查看dll窗口,拉到最下面就能看到sample.sys

卸载驱动(停止服务):

KdPrint 宏DbgPrint API的包装

通过在每个函数开头加入KdPrint(("Debug messgae"));可以观察函数调用的发生

使用DebugView,选择capture Kernel可以看到内核调试信息

创建一个驱动用于显示系统版本信息,使用RtlGetVersion

code:

shell:

研究一些内核的API、结构和定义,以及一些驱动程序中的机制

用户模式和内核模式调试的重要区别

未处理异常会导致蓝屏,原因是防止继续执行代码、对系统造成不可逆转的伤害

内核代码不应该跳过任何细节或错误检查

如果驱动程序卸载时仍保留分配的内存或打开的内核句柄,这些资源不会自动释放,只会在下次系统启动时释放

原因是驱动程序可以分配一些缓冲区,然后将其传递给另一个与之合作的驱动程序

忽略内核API的返回值很危险,应该总是检查返回值

中断请求级(Interrupt Request Level, IRQL)通常为0

用户模式下始终为0,内核模式下大部分时间为0

没有C++ runtime

一些不支持的C++特性:

一些支持的C++特性:

内核调试需要双机调试,一台作为调试者、另一台作为被调试者运行驱动程序

内核术语是 Checked(Debug)和 Free(Release)

Debug意味着可以使用DBG符号

内核API常用前缀的意义:

Nt前缀的内核函数对应NtDll.Dll的函数,会根据 KTHREAD 结构的标记(调用者是否来自内核)对参数进行检查

Zw前缀的内核函数先将调用者模式设为KernelMode(0),然后调用Nt前缀的内核函数

可以在ntstatus.h中找到NTSTATUS值的定义

大多数代码并不关心错误具体是什么,仅测试最高位即可,可以使用NT_SUCCESS

当返回到用户层时,会由STATUS_xxx转成ERROR_yyy,用户模式通过GetLastError可以得到这些错误

通常遇到错误时,会返回相同的 NTSTATUS 到调用函数

内核使用UNICODE_STRING

Length是字符串的字节数(不包括\x00\x00结束符)

MaximumLength是不需要重新分配内存的情况下、字符串字节数上限

需要注意的是,UNICODE_STRING并不总是有\x00\x00结尾

一些常用的字符串操作函数:

内核提供两种通用内存池(general memory pools)给驱动使用:

枚举类型POOL_TYPE表示池类型,只有三种是可以用于驱动的:
PagedPoolNonPagedPoolNonPagedPoolNx
(non-page pool没有可执行权限)

常用内存池函数:

tag是4字节的值

可以在PoolMon(WDK的Windows Kits中)中观察到有tag的内存池(tag以大端序字符串显示)

给ustring分配页池内存:

code:

shell:

内核使用循环双向链表:

CONTAINING_RECORD宏执行适当的偏移计算并转换为实际数据类型
CONTAINING_RECORD(pvoid, type, entry_member_name)

常用链表函数(时间复杂度都是常数):

后三个关于自旋锁,在第6章详细讨论

常用major function代码:

MajorFunction数组由内核初始化指向内核内部例程IopInvalidDeviceRequest,该例程直接返回失败,表示不支持该操作

驱动通过设备与r3代码通信,驱动应该至少创建一个设备对象并为其命名

CreateFile可以打开设备,第一个参数为设备对象名称

打开文件或设备的句柄会创建内核结构 FILE_OBJECT 的实例,这是个半文档化的结构。

更准确的说,CreteFile接受一个symbolic link(符号链接)

对象管理器中名为??的目录下的符号链接都可被用户模式代码通过CreateFile或Createfile2调用

可以通过WinObj查看(WinObj中目录名为Global??

使用符号链接的CreateFile的文件名(第一个参数),必须加上前缀\\.\(c++中是"\\\\.\\"

如果创建了多个设备对象,将形成一个单向链表,添加设备时是头插法,所以第一个创建的设备在链表的最后

将完成一个完整的驱动及客户端程序,利用驱动完成只能在内核模式下完成的功能(设置任意级别的线程优先级)

线程优先级 = 进程优先级 + 相对线程优先级

用户模式下,设置进程优先级可以用SetPriorityClass,共有6个级别
设置相对线程优先级可以用SetThreadPriority,共有7个级别

下面是线程优先级合法值的表(通过windows api设置),据别的书说是个未文档化的东西,windows不建议开发时考虑线程优先级,该表的值随windows版本变化可能发生改变

进程优先级枚举:级别+_PRIORITY_CLASS

线程优先级枚举:THREAD_PRIORITY_+级别

大多数驱动需要在DriverEntry中做如下操作:

所有驱动必须支持IRP_MJ_CREATEIRP_MJ_CLOSE,不然无法打开一个驱动的设备的句柄,通常这两个调度例程是相同的

调度例程的函数原型:NTSTATUS Function(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp)

用户模式客户端可用的三个基础函数:WriteFileReadFileDeviceIoControl

必须使用CTL_CODE宏来构建控制代码

示例:

创建设备名:

在创建一个设备对象前,需要先创建一个UNICODE_STRING存储内部设备名称

下面是两种初始化方式:

设备名称需要在设备对象管理器目录下

(RtlInitUnicodeString函数内部字符串的长度,RTL_CONSTANT_STRING宏在编译时计算长度)

创建设备对象:

创建设备对象需要调用IoCreateDevice

创建设备完整示例:

创建符号链接:

需要创建一个指向设备的符号链接,供r3调用

同样需要先创建一个字符串作为符号链接对象名称

注意:资源释放

上面创建的字符串会自动释放(好像在函数的栈中)?但对象不会,需要(在unload例程中)手动删除

将用CTL_CODE构造的控制代码放到一个头文件中,供驱动代码和用户模式客户端代码同时使用

通过符号链接获驱动的设备的句柄

该例程什么都不用做,直接返回成功即可

IRP是半文档化结构,通常来自运行中的管理器:I/O Manager, Plug & Play Manager or Power Manager

对驱动程序的每个请求总是包装在 IRP 中

IRP中有一个或多个IO_STACK_LOCATION结构

为了完成IRP,需要调用IoCompleteRequest,这个函数做很多东西,基本上理解为将IRP传播回创建者(通常是I/O管理器),然后由管理器通知客户端操作完成

调用IoGetCurrentIrpStackLocation获取当前设备对应的IO_STACK_LOCATION

IO_STACK_LOCATION中有控制代码、输入输出buffer指针等

调度例程运行在调用该例程的用户模式进程的上下文中

使用ULongToHandle(这实际上只是个casting)将pid转换成HANDLE

线程和进程存在一个全局私有内核句柄表,句柄的“值”实际上就是ID

(HANDLE在64位系统是64位,线程ID始终是32位)

start后可以在WinObj中的Driver目录下看到驱动、GLOBAL??目录下看到符号链接

可以在Process Explorer中查看进程的pid以及其线程的动态优先级

关于使用WinDbg进行调试

四个调试器:

WinDbg Preview是WinDbg的“最新版”,解决了一些WinDbg上的bug

这些调试器都是基于DbgEng.Dll

虽然有GUI,实际上还是命令行,所有UI操作都会转成命令,显示在命令行窗口上

WinDbg支持三种类型的命令:

符号信息:

设置符号的方法1:.symfix

设置符号的方法2:设置环境变量
_NT_SYMBOL_PATH=SRV*c:\Symbols*http://msdl.microsoft.com/download/symbols

lm:显示进程加载的模块,以及各模块是否加载了符号

.reload /f modulename.dll:强制加载模块的符号

!sym noisy:记录符号加载尝试的详细信息

线程:

~:显示调试进程中所有线程的信息
线程信息前的.表示当前线程,#表示触发中断的线程
输入提示冒号右边的数字是当前线程的索引

~ns:切换到索引为n的线程
可以组合命令~nk,这样可以在不切换线程的情况下,在别的线程执行操作(这里是显示别的线程的调用堆栈)

k:当前线程的调用堆栈(stack trace)

!teb:查看TEB的部分信息,默认当前线程的

进制转换:

16转10:

10转16:

数据或结构的显示:

dt [type]:显示数据结构的定义(如显示_TEB:dt ntdll!_teb

dt [type] [addr]:显示数据结构的数据(如显示某个_TEB:dt ntdll!_teb 000000`012229d000

r [reg]:读取寄存器(如读取rcx:r rcx

d{a|b|c|d|D|f|p|q|u|w|W}:以指定类型显示指定地址的数据
a:ascii字符
b,w,d,q:字节
u:unicode
f:单精浮点
D:双精浮点

u:显示反汇编,默认8句汇编指令

!error [error_code]:显示错误信息

断点和运行:

bp [symbol]:设置断点(如CreateFile:bp kernel32!createfilew

bl:显示当前设置的断点

bd:禁用断点,禁用所有断点:bd *

bc:删除断点

g(F5):运行直到断点

p(F10):步过

t(F11):步进

修改启动项:bcdedit /debug on

!process 0 0:显示所有进程的基本信息

!process指令后第一个数字是筛选特定进程,0表示所有进程;第二个数字是细节掩码,0表示最少细节;第三个参数是筛选可执行文件

.process /p [EPROCESS]:切换到指定进程

peb在用户模式地址空间中,查看peb需要先设置正确的用户模式进程环境

不切换的做法:.process /p ffff8d0e849df080; !peb e8a8c9c000

调用堆栈中,nt前缀表示内核

.reload /user:加载用户模式符号

其余常用/有趣的内核模式调试指令:

完全内核调试需要”双机“

最好的连接方式是通过网络,这需要主机和被调试目标系统版最少为Win8

另外一种方法是COM串口,大多数虚拟机支持虚拟串口而不需要真实(物理的)串口线

详细配置方式略过

调试器需要设置调试端口映射和命名管道,与虚拟机上的相同

输入提示kd左边的数字是引起中断的处理器的索引

可以设置未来断点(在运行程序前设置断点)

如设置驱动prioritybooster的入口点:bu prioritybooster!driverentry

可以设置只在指定进程上中断:bp /p [EPROCESS] [symbol]
如:bp /p ffffdd06042e4080 prioritybooster!priorityboosterdevicecontrol

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bcdedit /set testsigning on
bcdedit /set testsigning on
 
 
 
 
 
 
 
 
 
 
extern "C"
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath);
extern "C"
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath);
#include <ntddk.h>
 
void SampleUnload(_In_ PDRIVER_OBJECT DriverObject) {
    UNREFERENCED_PARAMETER(DriverObject);
}
 
extern "C"
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
    UNREFERENCED_PARAMETER(RegistryPath);
 
    DriverObject->DriverUnload = SampleUnload;
 
    return STATUS_SUCCESS;
}
#include <ntddk.h>
 
void SampleUnload(_In_ PDRIVER_OBJECT DriverObject) {
    UNREFERENCED_PARAMETER(DriverObject);
}
 
extern "C"
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
    UNREFERENCED_PARAMETER(RegistryPath);
 
    DriverObject->DriverUnload = SampleUnload;
 
    return STATUS_SUCCESS;
}
 
 
 
sc create sample type= kernel binPath= "\\vmware-host\Shared Folders\MyDriver\x64\Debug\sample.sys"
sc create sample type= kernel binPath= "\\vmware-host\Shared Folders\MyDriver\x64\Debug\sample.sys"
 
sc start sample
sc start sample
 
sc stop sample
sc stop sample
 
 
 
// Get Version
RTL_OSVERSIONINFOW versionInfo = { 0, };
versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOW);
RtlGetVersion(&versionInfo);
 
// Print
DbgPrint("[E1] Major:%d\n[E1] Minor:%d\n[E1] Build:%d",
    versionInfo.dwMajorVersion,
    versionInfo.dwMinorVersion,
    versionInfo.dwBuildNumber);
// Get Version
RTL_OSVERSIONINFOW versionInfo = { 0, };
versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOW);
RtlGetVersion(&versionInfo);
 
// Print
DbgPrint("[E1] Major:%d\n[E1] Minor:%d\n[E1] Build:%d",
    versionInfo.dwMajorVersion,
    versionInfo.dwMinorVersion,
    versionInfo.dwBuildNumber);
sc create E1_OSVersion type= kernel binPath= "\\vmware-host\Shared Folders\MyDriver\x64\Debug\E1_OSVersion.sys"
sc start E1_OSVersion
sc stop E1_OSVersion
sc create E1_OSVersion type= kernel binPath= "\\vmware-host\Shared Folders\MyDriver\x64\Debug\E1_OSVersion.sys"
sc start E1_OSVersion
sc stop E1_OSVersion
用户模式 内核模式
未处理异常 进程崩溃 系统崩溃
终止 当进程终止,所有内存和资源都会被自动释放 当驱动卸载,如果没有手动释放,会造成泄露直到重启
返回值 API错误有时候会忽略 应该不忽略任何错误
IRQL 总是 PASSIVE_LEVEL (0) 可能为更高
错误代码 通常只会影响本进程 影响整个系统
测试和调试 通常在开发机器上调试 需要双机调试
库(Lib) 可以使用C/C++库(如STL、boost) 大多数标准库无法使用
异常处理 可以使用C++异常或SEH 只能使用SEH
C++支持 完全的C++支持 不支持C++ runtime
 
 
 
 
 
 
 
 
 
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWCH Buffer;
} UNICODE_STRING;
typedef UNICODE_STRING *PUNICODE_STRING;
typedef const UNICODE_STRING *PCUNICODE_STRING;
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWCH Buffer;
} UNICODE_STRING;
typedef UNICODE_STRING *PUNICODE_STRING;
typedef const UNICODE_STRING *PCUNICODE_STRING;
 
 
 
 
 
 
 
UNICODE_STRING strA;
int length;
// allocate
strA.Buffer = (WCHAR*)ExAllocatePoolWithTag(PagedPool,
    length, 'dcba');
if (strA.Buffer == nullptr) {
    KdPrint(("Failed to allocate memory\n"));
    return STATUS_INSUFFICIENT_RESOURCES;
}
strA.MaximumLength = length;
UNICODE_STRING strA;
int length;
// allocate
strA.Buffer = (WCHAR*)ExAllocatePoolWithTag(PagedPool,
    length, 'dcba');
if (strA.Buffer == nullptr) {
    KdPrint(("Failed to allocate memory\n"));
    return STATUS_INSUFFICIENT_RESOURCES;
}
strA.MaximumLength = length;
sc create S2_DynMemAlloc type= kernel binPath= "\\vmware-host\Shared Folders\MyDriver\x64\Debug\S2_DynMemAlloc.sys"
sc start S2_DynMemAlloc
sc stop S2_DynMemAlloc
sc create S2_DynMemAlloc type= kernel binPath= "\\vmware-host\Shared Folders\MyDriver\x64\Debug\S2_DynMemAlloc.sys"
sc start S2_DynMemAlloc
sc stop S2_DynMemAlloc
typedef struct _LIST_ENTRY {
    struct _LIST_ENTRY *Flink;
    struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;
typedef struct _LIST_ENTRY {
    struct _LIST_ENTRY *Flink;
    struct _LIST_ENTRY *Blink;

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

收藏
免费 4
支持
分享
最新回复 (10)
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
不错,学习了
2022-1-19 11:13
0
雪    币: 160
活跃值: (19)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
大佬,我们这里安全公司缺人,要不要考虑一下?
2022-1-26 16:17
0
雪    币: 120
活跃值: (29)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
终于看到另一个也读这本书的人了
2022-1-26 18:15
0
雪    币: 1
活跃值: (120)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
国内Windows已经世风日下,国企在搞国产化,微软在往云上走。感觉Windows慢慢也没搞头了。
2022-2-2 01:09
1
雪    币: 5916
活跃值: (661)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
师傅我想问下学习Windows内核需要看哪些书籍
2022-2-2 09:38
0
雪    币: 5916
活跃值: (661)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
Code-X 国内Windows已经世风日下,国企在搞国产化,微软在往云上走。感觉Windows慢慢也没搞头了。
哈哈哈
2022-2-2 09:38
0
雪    币: 5485
活跃值: (4918)
能力值: ( LV9,RANK:143 )
在线值:
发帖
回帖
粉丝
8
鸿渐之翼 师傅我想问下学习Windows内核需要看哪些书籍
我也是学内核没多久,Windows Kernel Programming这本书作为入门挺不错的,再深入点我也不是很了解了,打好基础就去多关注一些论坛上的新文章吧
2022-2-12 14:53
0
雪    币: 248
活跃值: (1418)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
wx_御史神风 我也是学内核没多久,Windows Kernel Programming这本书作为入门挺不错的,再深入点我也不是很了解了,打好基础就去多关注一些论坛上的新文章吧
谭文的<windows内核编程> 也ok.我也在读这个
2022-2-16 04:53
0
雪    币: 33
活跃值: (244)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
10
这本书3年前就看过,里面课后题项目都做了.入门很好,就是很多东西没讲, 网络这块的就一点没有,现在win内核开发的岗位非常少,招人的要求都很高,光这本估计还不够入职门槛.这书现在有第2版,会有网络相关的内容,可以关注一下
2022-2-16 09:20
1
雪    币: 1641
活跃值: (3601)
能力值: (RANK:15 )
在线值:
发帖
回帖
粉丝
11
fishleong 这本书3年前就看过,里面课后题项目都做了.入门很好,就是很多东西没讲, 网络这块的就一点没有,现在win内核开发的岗位非常少,招人的要求都很高,光这本估计还不够入职门槛.这书现在有第2版,会有网络相关 ...
岗位并不少,要求也并不高,只是很多人觉得参加个培训,看本书,就可以拿高于其他计算机方向的薪资。
他们不屑于去工资低的地方,他们觉得他们选择windows内核开发,就理所应该拿钱多。
2022-2-16 09:30
0
游客
登录 | 注册 方可回帖
返回
//