首页
社区
课程
招聘
[翻译]关键的Windows内核数据结构一览(上)
发表于: 2018-1-8 21:55 28195

[翻译]关键的Windows内核数据结构一览(上)

2018-1-8 21:55
28195

译者注:近期一直在整理归纳知识体系,译几叠好文,写数篇心得,旨在温故而知新。放眼四壁,一时浩如烟海,月迷津渡。在翻译fuzzySecurity的Windows exploit开发系列教程第十部分时,觅得此文,甚佳,不敢独酌。

在我们的“Windows internals and debugging”课程中,学生经常会问我们这样的一些问题:Windows内核使用哪种数据结构来实现互斥量?。。本文试图通过描述Windows内核和设备驱动所使用的一些关键数据结构来回答这样的问题。

本文重点强调了系统中各种数据结构的关系,帮助读者在内核调试中进行导航。当阅读本文时,读者应该使用一个便捷的内核调试器来尝试调试命令、进行数据结构的实验。本文仅仅是一个参考,而并非新手向导。

对每种结构来说,本文提供了数据结构的一种高层次描述,同时也描述了数据结构中指向其他结构的关键字段的细节。合适的话,可以对该结构使用调试命令,实现对所提供的数据结构的巧妙操纵。大部分文中提到的数据结构均由内核的paged抑或non-paged pool分配空间,这也是内核虚拟地址空间的一部分。

下列数据结构会在文中进行描述,单击以查看详情。

Windows内核中的大部分数据结构都保存在链表中,在链表头中保存着指向链表元素项的指针。LIST_ENTRY结构用于实现这些循环双链表。LIST_ENTRY结构既可用于链表头也可以用作保存单个链表元素的表项结构。LIST_ENTRY结构较为典型的通过嵌入到大数据结构中,维持着链表中元素的关系。

调试命令dt -l命令会步过任何内嵌了该双链表的数据结构并显示链表中的所有元素。dldb命令会向前和向后步过双链表,也可以使用!dflink!dblink

APIs:

Windows内核使用ERPROCESS结构体来表示一个进程,其包含了所有内核需要去保存关乎该进程的信息。对每一个运行在系统中的进程包括System Process和System Idle Process来说,都有一个对应的EPROCESS结构,System Process和System Idle Process运行在内核中。

EPROCESS结构属于内核的执行体层,包含了进程的资源相关信息诸如句柄表、虚拟内存、安全、调试、异常、创建信息、I/O转移统计以及进程计时等。

指向System Process的EPROCESS结构的指针保存在nt!PsInitialSystemProcess,而System Idle Process的EPROCESS指针保存在nt!PsIdleProcess

任何进程都可以同时隶属于多个集合或组。例如,一个进程总是在系统中active进程列表中,一个进程可以属于内部运行着一个会话的进程集合,一个进程也可以是某个job的一部分。为了实现这些集合或组,EPROCESS结构通过不同的字段持有数个列表项。

ActiveProcessLink字段用于将该EPROCESS结构链入系统中active进程链表,该链表的头保存在内核变量中nt!PsActiveProcessHead。类似的,SessionProcessLinks字段用于将该EPROCESS结构链入到一个会话链表,链表头在MM_SESSION_SPACE.ProcessListJobLinks字段用于将该EPROCESS结构链入到所属的job链表中,链表头在EJOB.ProcessListHead。内存管理器全局变量MmProcessList通过MmProcessLinks字段链入了一个进程链表。该链表可以通过MiReplicatePteChange()横贯以更新内核模式中关于进程虚拟地址空间的那部分。

属于进程的所有线程链表保存在ThreadListHead中,线程通过ETHREAD.ThreadListEntry排队。

内核变量ExpTimerResolutionListHead持有一个进程链表,使用NtSetTimerResolution()来改变定时器间隔。该链表被ExpUpdateTimerResolution()函数使用来更新时间分辨率到所有进程需求值中最小的那个。

!process命令从EPROCESS结构展示信息。.process命令切换调试器的虚拟地址空间上下文到特定的进程,当在一个完全的内核转储中或现场使用内核调试器时进行用户模式虚拟地址的实验时,这是一个非常危险的操作。

APIs:

KPROCESS结构内嵌在EPROCESS结构体中,保存在EPROCESS.Pcb字段,它被执行体层下一级的微内核层使用,包含了线程的调度、配额、优先级以及执行时间等信息。

ProfileListHead字段包含了为该进程创建的性能对象链表。该链表被性能中断所使用来记录相关性能的说明。

ReadyListHead字段是一个线程链表,保存的是进程中出于就绪状态的线程。只有当进程不在内存中时,该链表才是非空的。链表中每项都是指向KTHREAD对象的WaitListEntry域的地址。

译者注:这里我扩展解释一下该字段:记录了这个进程中处于就绪状态但尚未被加入全局就绪链表的线程。当进程被换出内存后,他所属的线程一旦就绪,则被挂入到此链表,并要求换入该进程;此后当进程被换入内存时,ReadyListHead中的所有线程被加入到系统全局的就绪线程链表中。

ThreadListHead字段是进程所有线程的链表。KTHREAD数据结构通过KTHREAD.ThreadListEntry链入。内核用它来遍历进程中所有的线程。

JobLinks字段是同属于一个job的进程链表,链表头在EJOB.ProcessListHead

Windows内核使用ETHREAD结构来表示一个线程,每个线程都有一个ETHREAD结构,这也包括在System Idle Process中的线程。

ETHREAD结构属于内核的执行体层,它包含了其他执行体组件诸如I/O管理器、安全引用监视器、内存管理、ALPC管理器等需要保存的线程相关信息。

Tcb字段包含了KTHREAD结构体,它嵌入到ETHREAD中并被用来存储线程调度相关信息。

每个进程都存储了一个ETHREAD结构体链表,代表了在进程中执行的线程,它在EPROCESS结构体的ThreadListHead字段中。

ETHREAD结构体通过ThreadListEntry字段链入到链表。

KeyedWaitChain字段用于保存那些正在等待一个特定事件的线程。

IrpList是一个IRP链表,用于表示该线程生成的I/O请求,在系统中这些请求在各驱动中正在处理但尚未完成。当线程终止时,这些IRP请求需要被取消。

CallbackListHead字段用于保存一个注册表回调函数的链表,它们会被调用以便于通知注册表过滤驱动关于该线程正在执行的注册操作。该字段对前向和后向通知注册表回调函数都是有效的。

Win32StartAddress字段是线程顶层函数的地址。该函数会通过CreateThread()传递给用户模式线程,通过PsCreateSsytemThread()给内核模式线程。

ActiveTimerListHead字段是个链表头,链表中包含了所有的当前线程active定时器(在一个确定的间隔后超期)。ActiveTimerListBlock字段用于保护链表,函数ExpSetTimer()通过ETIMER.ActiveTimerListEntry字段将定时器对象插入此链表。

调试命令!thread展示了线程的信息。.thread命令切换调试器CPU寄存器上下文到一个特定的线程。

APIs:

KTHREAD结构体内嵌在ETHREAD结构体中,存储在ETHREAD.Tcb字段,它被执行体层的下层微内核层所使用,它包含了线程的堆栈、调度、APC、系统调用、优先级、执行时间等信息。

QueueListEntry字段用于将关联到一个KQUEUE数据结构的线程链入链表。KQUEUE.ThreadListHead是该链表的头。KQUEUE结构体用于实现执行体工作队列(EX_WORK_QUEUE)以及I/O完成端口。当当前工作线程在此队列中且处于等待状态时,像KiCommitThreadWait()这样的函数会使用它来激活工作队列中的其他线程。

MutantListHead字段用于保存一个线程所获取的所有互斥量的链表。函数KiRundownMutants()使用此链表来检测一个线程在终止前是否释放了所有的互斥量,如果未能释放,则它会使得系统崩溃,bugcheck为THREAD_TERMINATE_HELD_MUTEX

Win32Thread字段指向了Win32K.sys结构体W32THREAD(指向由Win32子系统管理的区域)。当一个用模式线程转换到UI线程,它会发起一个到USER32或GDI32中API的调用。函数PsConvertToGuiThread()执行这一转换。Win32K.sys函数AllocateW32Thread()调用PsSetThreadWin32Thread()来设置Win32Thread字段的值。每个线程分配的结构体尺寸存储于Win32K.sys中的W32ThreadSize变量中。

WaitBlock字段是一个4个KWAIT_BLOCK数组结构,线程用来等待本地内核对象。KWAIT_BLOCK的其中之一是保留的,它用于实现超时等待,因此它只能指向KTIMER对象。

WaitBlockList字段指向了KWAIT_BLOCK数组结构,下才能用来等待一到多个对象。该字段由函数KiCommitThreadWait()于线程刚刚进入到它的等待状态时设置。如果我们的线程等待的对象数量少于THREAD_WAIT_OBJECTS(3),WaitBlockList就应该指向内置的WaitBlock[]数组,如果等待对象的数量超过了THREAD_WAIT_OBJECTS,但少于MAXIMUM_WAIT_OBJECTS(64),WaitBlockList应该指向一个外部分配的KWAIT_BLOCKS数组。ObpWaitForMultipleObjects()是用来分配带有标签'Wait'的KWAIT_BLOCK数组的其中一个函数。

WaitListEntry字段用于添加KTHREAD结构体到链表中,这些线程均已在特定CPU上进入了等待状态。内核进程控制区域结构体(KPRCB)的WaitListHead字段通过KTHREAD.WaitListEntry链接了这样的线程到一起。函数KiCommitThreadWait()添加线程,KiSignalThread()移除线程。

KPCR表示内核进程控制区域。它包含了每个CPU的信息,被内核和HAL所共享。系统有几个CPU,就有几个KPCR

当前CPU的KPCR总是可以通过fs:[0]在x86系统上访问,x64系统上则通过gs:[0]。通用的内核函数诸如PsGetCurrentProcess()KeGetCurrentThread()会利用FS/GS相对访问来从KPCR中获取信息。

Prcb字段包含了一个内嵌的KPRCB结构体,用于表示内核进程控制块。

一旦一个中断或异常发生,中断服务例程(ISRs)就会在CPU上执行。中段描述符表(IDT)是一个CPU定义的数据结构,指向了内核注册的ISRs。当中断或异常发生时,IDT被CPU硬件所使用来查找ISR并进行分发。IDT有256个表项,每个都指向一个ISR。中断向量是IDT中每个特定槽的索引值。KINTERRUPT结构体表示一个驱动注册的某个中断向量的ISR。

字段DispatchCode是一个字节数组,它包含了中断服务码的一些说明。特定向量的IDT条目直接指向了DispatchCode数组,调用DispatchAddress指向的函数。该函数一般是KiInterruptDispatch(),它负责建立一个需要去调用驱动提供的ISR的环境,该ISR由ServiceRoutine字段提供。

对消息信号中断(MSIs)来讲,ServiceRoutine指向了内核包装器函数KiInterruptMessageDispatch(),它通过MessageServiceRoutine指向的驱动提供的MSI中断服务例程来调用。

ActualLock字段指向一个自旋锁,在调用驱动支持的ISR之前用于SynchronizeIrql字段获取IRQL。

因为中断共享PCI总线的多重KINTERRUPT数据结构可以被注册为一个单一中断请求线(IRQ)。每个共享的中断向量的IDT条目都指向了第一个KINTERRUPT结构体,其他的KINTERRUPT结构体通过字段InterruptListEntry形成链。

调试器!idt -a命令展示了全部的每CPU中断描述表。

APIs:

CONTEXT结构体存储了异常上下文依赖于CPU的部分,它由CPU寄存器组成并被KiDispatchException()这样的函数用来下发异常。

CONTEXT结构体的部分内容由KeContextFromKframes函数捕获的KTRAP_FRAME结构体组成。同样地,在异常被分发后,CONTEXT结构体中被修改的内容会被KeContextToKframes()改回原貌。这一机制用于实现结构化异常处理(SEH)。

ContextFlags字段是一个位掩码,决定了CONTEXT结构体的哪些字段包含有效的值。例如CONTEXT_SEGMENTS指示上下文结构体中段寄存器是有效的。

调试器的.cxr命令用于切换调试器当前寄存器上下文,加载存储的CONTEXT结构体的值。

API:

KTRAP_FRAME用于在中断或异常发生时保存CPU寄存器的内容。KTRAP_FRAME结构体一般在线程的内核模式栈上分配。陷阱帧的一小部分由CPU组成,一部分由自身的中断和异常控制组成,剩下的那些由软件异常和硬件中断handler提供,Windows下诸如函数KiTrap0E()KiPageFaultKiInterruptDispatch()。在64位CPU上,陷阱帧的某些包含非优化(non-volatile)寄存器值的字段不是由异常handler构成的。

调试器的.trap命令切换太欧式器当前寄存器上下文到存储的KTRAP_FRAME结构。

DPC例程用于延迟中断进程到IRQL的DISPATCH_LEVEL。它会降低特定CPU在高IRQL例如DIRQLx上的运行时间。DPC也被用来提醒内核组件超时的定时器。ISRs和定时器都需要DPC。

译者注:延迟过程调用是Windows下一个很重要的机制,这个东西不仅用于定时器超时处理,还用于实现类似linux中中断下半部分sortirq的机制。·

KDPC表示一个延迟过程调用(DPC)数据结构,包含一个指向了驱动提供的例程。它应该IRQL为DISPATCH_LEVEL优先级时在任意线程上下文中被调用。

和中断服务例程不同之处在于,中断服务例程在线程栈上执行,DPC例程在per-CPU DPC栈上执行,它存储在KPCR.PrcbData.DpcStack

DEVICE_OBJECT结构有一个KDPC结构体内置在Dpc字段,用来从ISR请求DPC例程。

KDPC结构体持有一个per-CPU DPC队列。KPCR数据结构的PrcbData.DpcData[0]字段包含了链表头。KDPCDpcListEntry字段用来保存链表中的DPC。

调试命令!pcr!dpcs显示了单一进程的DPC例程。

APIs:

异步过程调用例程在特定线程的上下文、PASSIVE_LEVEL或APC_LEVEL优先级上执行。这些例程被驱动用来执行特定进程上下文的行为,主要是访问进程的用户模式虚拟地址空间。Windows中具体的功能如附加和分离一个线程到进程以及线程悬挂都是基于APC实现。APC有3种类型:用户模式;普通内核模式;特殊内核模式。

KAPC表示了异步过程调用(APC)结构体,它包含一个指向驱动支持的例程的指针。当APC可以下发给该线程的时候,该例程会在此特定线程上下文的PASSIVE_LEVEL或APC_LEVEL优先级上执行。

KTHREAD.ApcState.ApcListHead[]数组的两个表项包含了用户模式和内核模式的线程悬挂的APC列表。KAPC结构通过字段ApcListEntry链入此结构。

设置KTHREAD.SpecialApcDisable为一个负数会引起线程的特殊和普通内核APC被禁用。

设置KTHREAD.KernelApcDisable位一个负数会引起线程的普通内核APC被禁用。

NormalRoutine字段对特殊内核APC来说是NULL。对普通内核APC来说,它指向的函数运行在PASSIVE_LEVEL。

KernelRoutine字段指向了在APC_LEVEL执行的函数。

RundownRoutine字段指向了一个函数,当APC在线程终止被丢弃时会被执行。

调试命令!apc用以扫描系统中所有线程的悬挂APC并显示。

APIs:

Windows内核允许线程附加到不同的进程中而不必是原始创建它的那个进程。这允许线程去获取对另外的进程的用户模式虚拟地址空间的临时访问。当线程附加到其他进程时,KAPC_STATE用来保存的线程的APC列表。因为APC是线程(以及进程)特定的,当一个线程附加到一个不同于当前所在进程的进程时,它的APC状态数据需要被保存。这是必须的因为线程当前队列中的APC(需要知道原始进程的地址)不能被下发给新的进程上下文。KTHREAD结构有两个内置的KAPC_STATE对象:一个是线程原始进程,另一个是线程附加的进程。在线程执行堆栈附加事件中,调用者需要提供更多KAPC_STATE结构体的存储空间来保存当前APC状态变量并可以转移到新的APC环境上。

ApcListHead数组的两个成员分别是用户模式和内核模式的线程悬挂APC队列。KAPC结构体通过ApcListEntry字段链入到链表中。

APIs:

Windows本地内核对象是一些数据结构。它们种类繁多,且可以被线程直接通过调用KeWaitForSingleObject()来等待。内核中有着这些结构的逻辑实现,大部分结构都通过用户模式应用程序的本地(Nt/Zw)Win32 API函数暴露出去。事件、信号量、互斥量、定时器、线程、进程以及队列都是本地内核对象的例子。

DISPATCHER_HEADER结构内嵌到每一个本地内核对象中,它是一个线程等待机制实现中非常关键的组件。

每一个KTHREAD结构体都包含一个内置的KWAIT_BLOCK数组结构,它用于让线程在本地内核对象上阻塞。DISPATCHER_HEADERWaitListHead字段指向了一个KWAIT_BLOCKS链结构,链中每个成员表示了线程所等待的某个本地内核对象。KWAIT_LOCK_WaitListEntry字段用于保存处于链表中的KWAIT_BLOCK结构。当本地内核对象被通知时(Signaled),一到多个KWAIT_BLOCK会从链表中移除,其包含的线程会被置入就绪态。

Type字段用于识别内嵌的DISPATCHER_HEADER所包含的对象类别。它是枚举类型nt!_KOBJECTS的前10个值中的一个。该字段决定了DISPATCHER_HEADER的其他字段应该如何被解析。

Lock字段(bit7)实现了一个对象特定自定义的自旋锁,用以保护SignalStateWaitListFields字段。SignalState字段决定了该对象是否已通知(Signaled)。

APIs:

KEVENT表示了内核事件数据结构。事件有两种类型:异步(自动重置)和通知(手动重置)。当一个异步事件被某线程通知(Signaled)时,等待它的线程中仅有一个会进入就绪态;但是当一个通知事件被某个线程通知时,等待它的所有线程都会被置入就绪态。KEVENTS可以作为独立的数据结构存在,用KeInitializeEvent()初始化或者作为事件对象由ZwCreateEvent(),一个本地被内核API内部使用的函数IoCreateSynchronizationEvent()IoCreateNotificationEvent()创建。

APIs:

KSEMAPHORE表示内核信号量数据结构。信号量由线程调用KeWaitForSingleObject()进行获取。如果已经有一定数量的线程获取了信号量使得信号量超出了使用限制,后续的线程调用KeWaitForSingleObject()时就会进入等待状态。一旦任何线程使用KeReleaseSemaphore()释放了信号量,一个等待的线程就进入准备执行的状态。

线程的数量也就是获取了信号量的个数存储在字段Header.SignalStateLimit字段用于存储可同时持有该信号量的最大数量的线程。

APIs:

KMUTANT表示一个内核互斥量数据结构。一个互斥量只能被单一线程在一个时间点拥有,但同一线程可以递归的多次获取这个互斥量。

OwnerThread字段指向了线程的KTHREAD结构,该线程持有互斥量。

每个线程持有一个获取的所有互斥量组成的链表,KTHREAD.MutantListHead是该链表的头,MutantListEntry字段用于链入该链表。

ApcDisable字段决定了互斥量是一个用户模式还是内核模式对象,值0表示用户模式互斥量,其他任何值都表示是一个内核模式互斥量。

Abandoned字段会在互斥量没有被释放而被删除时设置。这将抛出一个STATUS_ABANDONED异常。

APIs:

KTIMER表示一个定时器数据结构。当一个线程睡眠或等待一个分发对象且持有一个超时值时,Windows内核内部使用KTIMER来实现这一等待。

内核持有一个数组KiTimerListHead[],它包含512个链表头,每个头存储一个KTIMER对象链表,他们会在一个确定的时间超时。TimerListEntry字段用于将KTIMER链入队列。

当一个定时器超时时,它要么唤醒一个在该定时器等待的线程,要么调度一个DPC例程来通知一个驱动该定时器已超时,指向DPC数据结构的指针存放在Dpc字段。定时器可以是偶然发生(超时1次)或周期性发生(在被取消之前重复不断的超时)。

调试器的!timer命令用以显示系统中所有的活动KTIMER

APIs:

KGATE表示一个内核门对象。KGATE提供的功能和异步类型的KEVENT十分相似,然而KGATEKEVENT更为高效。

当一个线程在等待诸如事件、信号量、互斥量等分发对象时,它会使用KeWaitForSingleObject()或其变形函数,这些函数都是通用函数,并且需要处理所有和线程等待相关的特殊情况,例如,警告(alerts), APCs, 工作线程唤醒等。另一方面,在KGATE上等待是通过一个特殊的函数KiWaitForGate()来完成的,它不满足所有特殊的情况,使得代码路径非常的高效。但是,使用专用API的缺点是,线程不能同时在KGATE对象和另一个分发对象上等待。

KGATE APIs被内核内部使用,不会导出给驱动调用。KGATE在内核的内部多出被使用,这其中包括实现的守卫互斥锁(guarded mutexes)。当互斥量不可用时,守卫互斥锁内部等待一个KGATE对象。

APIs:

KQUEUE表示一个内核队列数据结构。KQUEUE用于实现执行体工作队列、线程池以及I/O完成端口。

多个线程可以经调用KeRemoveQueueEx()函数同时等待一个队列。当一个队列条目(任何内嵌了LIST_ENTRY的数据结构)被插入到队列时,其中的一个等待线程会被唤醒并在从队列中取出条目后,得到一个指向该队列条目的指针。

通用内核等待函数诸如KeWaitForSingleObject(),KeWaitForMultipleObjects()中有特殊的逻辑来处理那些关联一个队列的线程。每当这样的线程在队列以外的分发对象上等待时,与队列关联的另一个线程将被唤醒,以处理来自队列的后续项目。这可以确保在队列中插入的条目能够尽快得到服务。

EntryListHead字段是使用内嵌的LIST_ENTRY字段插入到队列形成的链表的头。函数KiAttemptFastInsertQueue()负责插入条目,KeRemoveQueueEx()负责移除条目。

ThreadListHead字段指向关联到该队列的所有线程链表。对所有这样的线程,其KTHREAD.Queue字段指向了该队列。

CurrentCount字段包含了正在积极处理队列项的线程数量,该数量被MaximumCount字段值所限制,该值的设置会根据系统上CPU的数量。

APIs:

驱动程序使用工作项将某些例程的执行延迟到内核工作线程,这些线程在PASSIVE_LEVEL优秀级上会调用驱动程序提供的例程。工作项包含指向驱动提供的工作例程的指针,这些工作例程由驱动排列成一个固定的内核工作队列集合。内核提供工作线程例如ExpWorkerThread()等通过出队列条目并调用工作例程的方式来服务这些工作队列。IO_WORKITEM结构用以表示一个工作项。

内核变量nt!ExWorkerQueue包含了一个含3个EX_WORK_QUEUE结构体成员的数组,分别表示系统中关键的(Critical也叫临界的),延迟的(Delayed)和超临界(HyperCritical)工作队列。WorkItem字段用于组织IO_WORK_ITEM结构成上面3个中的某一个工作队列。

函数IoQueueWorkItemEx()持有一个IoObject的引用,它是一个指向驱动或设备对象的指针,用以防止驱动在工作例程执行期间被卸载。

WORK_QUEUE_ITEM结构体内嵌的WorkerRoutine字段指向了I/O管理器,它提供了名为IopProcessWorkItem()的包装器函数,该函数调用驱动程序提供的工作例程,并降低IoObject的引用计数。

Routine字段指向了驱动提供的工作例程,它在PASSIVE_LEVEL优先级上执行。

调试器的!exqueue命令显示了关于工作队列和工作线程的详细信息。

APIs:

译者注:由于本文篇幅实在过巨,且译者英文较poor, 为了防止通篇又臭又长的译文劝退读者,故于此拦腰截断,近日将更新下半部分。
下篇已译毕,单击此处

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 6
支持
分享
最新回复 (9)
雪    币: 3738
活跃值: (3872)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
2
楼主辛苦了!
2018-6-25 11:54
0
雪    币: 210
活跃值: (68)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
好文采  感谢
2018-10-31 11:47
0
雪    币: 300
活跃值: (2472)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
mark
2018-10-31 11:56
0
雪    币: 6064
活跃值: (12624)
能力值: ( LV12,RANK:312 )
在线值:
发帖
回帖
粉丝
5
幸苦了 好文章!
2018-12-28 20:53
0
雪    币: 2391
活跃值: (309)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
6
mark
2018-12-29 09:03
0
雪    币: 83
活跃值: (1087)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
7
学习了 很好
2019-7-21 15:15
0
雪    币: 0
活跃值: (150)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
mark
2019-9-2 11:16
0
雪    币: 2980
活跃值: (4891)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
9
学习了
2020-3-17 00:43
0
雪    币: 1810
活跃值: (4020)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
mark
2023-1-16 12:44
0
游客
登录 | 注册 方可回帖
返回
//