找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 167|回复: 0

遍历内核中的DpcTimer

[复制链接]

210

主题

371

回帖

0

积分

管理员

积分
0
发表于 2013-10-12 12:12:13 | 显示全部楼层 |阅读模式
[bgcolor=#f5f5f5]其实本篇帖子是对于之前一篇帖子的补完 [/bgcolor][bgcolor=#f5f5f5]http://bbs.pediy.com/showthread.php?t=140344[/bgcolor]

[bgcolor=#f5f5f5]遍历系统内核DpcTimer是一个比较烦人的事情,记得当初弄的时候蓝脸N次
而且作为系统底层内核中的底层,遍历它也不是那么安全的事情,
主要因为不知道有没有相关的系统锁能够让我们在遍历的时候加以利用,
来使我们的操作更加安全,希望有相关信息的朋友能指点我一下,呵呵

当初在遍历DpcTimer的时候网上都没搜到相关信息,
后来对某工具进行了一下逆向,才找到的线索
写这篇帖子之前又搜了一下,已经有零星的介绍,大家可以去搜索一下

废话不多说了,DpcTimer这条链在xp 2k3 win7下的遍历方法都有些许的不同
首先看 win xp

我们还是从已知入手:
我们的驱动可以注册Dpc定时器的,系统接受我们的注册之后就应当会把它放入一个链表中.
所以先看一下注册Dpc定时器的相关函数:
KeInitializeDpc    初始化一个DPC对象
KeSetTimerEx    设置定时器,其中可以加入DPC对象

KeSetTimerEx之后我们的DPC过程就能在指定的时间被调用了,说明已被加入了系统底层链表中
我们在IDA中打开 xp 的ntkrnlpa.exe, 定位到KeSetTimerEx的实现部分:

[/bgcolor]
[bgcolor=#f5f5f5][/bgcolor]


图片:1.jpg
[bgcolor=#f5f5f5]这里并没有发现有对于系统级全局变量的引用,[/bgcolor]
[bgcolor=#f5f5f5]不过通过观察名称发现KiInsertTreeTimer应该与插入一个定时器到链表有点关系.看一下:[/bgcolor] [bgcolor=#f5f5f5]
[/bgcolor]



图片:02.jpg
[bgcolor=#f5f5f5]对于参数进行了一些处理之后调用了KiInsertTimerTable,继续追寻:
[/bgcolor]



图片:03.jpg
[bgcolor=#f5f5f5]这里对全局变量KiInsertTimerTable进行了引用,看名称就是全局的定时器链表了[/bgcolor]


[bgcolor=#f5f5f5]同时我们在这里也可以发现一些其他的线索,[/bgcolor]
[bgcolor=#f5f5f5]在调用了KiComputeTimerTableIndex之后直接使用其返回值进行KiTimerTableListHead对应项的索引,[/bgcolor]
[bgcolor=#f5f5f5]而步进是8,所以KiTimerTableListHead应该就是8字节为大小的结构数组的首地址,[/bgcolor]
[bgcolor=#f5f5f5]而这个结构数组有多少成员呢?[/bgcolor]

[bgcolor=#f5f5f5]看一下有那些地方对KiTimerTableListHead进行了引用,在IDA中的KiTimerTableListHead上按x可以看到:
[/bgcolor]



图片:04.jpg
[bgcolor=#f5f5f5]其中有KiInitSystem对其的引用,应该就是对于结构数据的初始化地方了:
[/bgcolor]



图片:05.jpg
[bgcolor=#f5f5f5]这里我们就得到了两个重要的信息:[/bgcolor]
[bgcolor=#f5f5f5]1是这个结构数组项数为 0x100 = 256[/bgcolor]
[bgcolor=#f5f5f5]2是这个结构数组每一项都是一个ListEntry结构,因为这里出现了明显的初始化片段[/bgcolor]

[bgcolor=#f5f5f5]所以我们遍历这256个ListEntry就能得到所有的定时器了,[/bgcolor]
[bgcolor=#f5f5f5]到现在唯一还需要得知的就是ListEntry上每一个结构的定义形式,[/bgcolor]
[bgcolor=#f5f5f5]而这个结构在WDK中已经定义好了,就是KeSetTimerEx传入的KTIMER[/bgcolor]
代码:
复制代码
  • typedef struct _KTIMER {
  •   DISPATCHER_HEADER  Header;
  •   ULARGE_INTEGER    DueTime;  //+0x10
  •   LIST_ENTRY      TimerListEntry;
  •   PKDPC        Dpc;    //+0x20
  •   LONG        Period;    //+0x24
  • } KTIMER, *PKTIMER, *PRKTIMER;
最后我们查找到KiTimerTableListHead然后遍历就行了,不过上面的查找路线过于曲折了,
再看一下其他地方对KiTimerTableListHead的引用:



图片:06.jpg
KeUpdateSystemTime是内核导出的函数,在这里查找就方便多了:



图片:07.jpg
可以通过搜索特征码来找到它,不过在这里我用了反汇编引擎,呵呵
通过观察,对KiTimerTableListHead的引用的lea指令是在KeUpdateSystemTime中出现的第一个lea指令,
因此利用反汇编引擎得到第一个lea指令地址就能定位到KiTimerTableListHead了

相关代码:
代码:

复制代码
  • // 取KeUpdateSystemTime地址
  •   RtlInitUnicodeString(&destString,(PWCHAR)L"KeUpdateSystemTime");
  •   Address = (ULONG)MmGetSystemRoutineAddress(&destString);
  •   if (Address == 0) return 0;

  •   // 反汇编找到C_LEA<就是 lea 指令>首次出现的地址
  •   Address = DisAsmFindFirstSpecialInstructionAddress(Address, C_LEA);
  •   if ( Address == 0 ) return 0;
  •   if ( *(PUSHORT)Address != 0x0C8D ) return 0;

  •   Address = *(PULONG)(Address + 3);
  •   if ( MmIsAddressValidEx((PVOID)Address) == VCS_INVALID ) return 0;

这里插个小广告,反汇编引擎我使用的是自己精简的OD的引擎,各位需要的话在这里可以找到:
http://bbs.pediy.com/showthread.php?t=140587

DisAsmFindFirstSpecialInstructionAddress的实现部分:

代码:
复制代码
  • ULONG DisAsmFindFirstSpecialInstructionAddress(ULONG BeginAddress, ULONG CmdType)
  • {
  •   ULONG    DecodedLength = 0;
  •   ULONG    dw = 0;
  •   ULONG    Address = 0;
  •   Disasm    dis;

  •   while (TRUE)
  •   {
  •     if ( MmIsAddressValidEx((PVOID)(BeginAddress + DecodedLength)) == VCS_INVALID ) return 0;

  •     dw = DisasmCode((PUCHAR)(BeginAddress + DecodedLength),36,&dis);
  •     DecodedLength = DecodedLength + dw;
  •     if ( dis.cmdtype != CmdType ) continue;

  •     Address = BeginAddress + DecodedLength - dw;    //返回指令地址 此时指向指令
  •     break;
  •   }

  •   if ( MmIsAddressValidEx((PVOID)Address) == VCS_INVALID ) Address = 0;
  •   return Address;
  • }
找到KiTimerTableListHead之后遍历即可:


代码:


复制代码
  • //自定义的回传DPC TIMER结构
  • typedef struct _MyDpcTimer{
  •   ULONG  TimerAddress;    //KTIMER结构地址
  •   ULONG  Period;        //循环间隔
  •   ULONG  DpcAddress;      //DPC结构地址
  •   ULONG  DpcRoutineAddress;  //例程地址
  • }MyDpcTimer,*PMyDpcTimer;

  • #define MAX_DPCTIMER_COUNT 250

  • #pragma PAGECODE
  • static ULONG GetDpcTimerInformation_XP(PVOID* pvBuf)
  • {
  •   ULONG  NumberOfTimerTable;
  •   ULONG  i;
  •   ULONG  ulCount = 0;
  •   PLIST_ENTRY  pList = NULL;
  •   PLIST_ENTRY pNextList = NULL;
  •   MyDpcTimer  MyDpc;
  •   PKDPCTIMER  pTimer = NULL;

  •   NumberOfTimerTable = 0x100;                                  //_KTIMER_TABLE_ENTRY数量
  •   pList = (PLIST_ENTRY)GetDpcTimerListHeadForXp_2K3();                    //取得链表头
  •   if (pList == NULL) return 0;

  •   *pvBuf = CallExAllocatePoolWithTag(PagedPool,MAX_DPCTIMER_COUNT*sizeof(MyDpcTimer),652);    //分配足够大的缓冲
  •   if (*pvBuf == NULL) return 0;
  •   RtlZeroMemory(*pvBuf,MAX_DPCTIMER_COUNT*sizeof(MyDpcTimer));

  •   for ( i = 0; i < NumberOfTimerTable; i++, pList++ )                      //NumberOfTimerTable 个list
  •   {
  •     if ( MmIsAddressValidEx((PVOID)&pList) == VCS_INVALID ) goto __exit1;


  •     if ( MmIsAddressValidEx((PVOID)pList->Blink) == VCS_INVALID ) continue;          //如果listentry域地址无效,continue
  •     if ( MmIsAddressValidEx((PVOID)pList->Flink) == VCS_INVALID ) continue;


  •     for ( pNextList = pList->Blink; pNextList != pList; pNextList = pNextList->Blink )    //遍历blink链
  •     {
  •       pTimer = CONTAINING_RECORD(pNextList,KDPCTIMER,TimerListEntry);            //得到结构首
  •       
  •       if ( MmIsAddressValid((PVOID)pTimer) &&
  •          MmIsAddressValid((PVOID)pTimer->Dpc) &&
  •          MmIsAddressValid((PVOID)pTimer->Dpc->DeferredRoutine) &&
  •          MmIsAddressValid((PVOID)&pTimer->eriod) )                    //过滤
  •       {
  •       
  •         RtlZeroMemory(&MyDpc,sizeof(MyDpcTimer));                    //准备更新结构信息
  •         if ( !MmIsAddressValid((PVOID)pTimer) ) break;
  •         MyDpc.TimerAddress = (ULONG)pTimer;

  •         if ( !MmIsAddressValid((PVOID)pTimer->Dpc) ) break;
  •         MyDpc.DpcAddress = (ULONG)pTimer->Dpc;

  •         if ( !MmIsAddressValid((PVOID)pTimer->Dpc->DeferredRoutine) ) break;
  •         MyDpc.DpcRoutineAddress = (ULONG)pTimer->Dpc->DeferredRoutine;

  •         if ( !MmIsAddressValid((PVOID)&pTimer->eriod) ) break;
  •         MyDpc.Period = pTimer->eriod;

  •         RtlMoveMemory( (PVOID)((ULONG)*pvBuf+ulCount*sizeof(MyDpcTimer)),&MyDpc,sizeof(MyDpcTimer) );
  •         ulCount++;
  •         
  •         // 简单退出了,需要更改为重新分配更大的缓冲
  •         if (ulCount >= MAX_DPCTIMER_COUNT) goto __exit1;
  •       }

  •       if ( !MmIsAddressValid(pNextList->Blink) ) break;                  //过滤

  •     }// end for
  •   }//end for

  •   return ulCount * sizeof(MyDpcTimer);

  • __exit1:
  •   if (*pvBuf != NULL) ExFreePool(*pvBuf);
  •   *pvBuf = NULL;
  •   return 0;
  • }
一直到如上进行了这么多的过滤,最后得到的结果才和xt的差不多了,大家可以根据需要自己修改



图片:08.jpg



图片:09.jpg
虽然 win 2k3 用的人不多,也简单说下,因为有wrk,我们直接在这里搜索KiTimerTableListHead查找线索
于是找到:
代码:

复制代码
  • #define TIMER_TABLE_SIZE 512

  • typedef struct _KTIMER_TABLE_ENTRY {
  •     LIST_ENTRY Entry;
  •     ULARGE_INTEGER Time;
  • } KTIMER_TABLE_ENTRY, *PKTIMER_TABLE_ENTRY;

  • extern DECLSPEC_CACHEALIGN KTIMER_TABLE_ENTRY KiTimerTableListHead[TIMER_TABLE_SIZE];

结构数组项数被扩展成 512 个而且每个项添加了Time域,对应的KiInitSystem中的初始化片段:



图片:010.jpg
可见Time.LowPart初始化成0,Time.HighPart初始化成0xFFFFFFFF (没有使用的Entry都会设置为0xFFFFFFFF,可以当作一个过滤条件)

在 2k3 中KiTimerTableListHead在KeSetTimerEx直接被引用,可以在这里查找:


图片:011.jpg
[bgcolor=#f5f5f5]我是使用反汇编引擎查找首个 add 指令来实现的[/bgcolor]

代码:
复制代码
  • RtlInitUnicodeString(&destString,(PWCHAR)L"KeSetTimerEx");
  •   Address = (ULONG)MmGetSystemRoutineAddress(&destString);
  •   if (Address == 0) return 0;

  •   // C_ADD 即表示 add 指令
  •   Address = DisAsmFindFirstSpecialInstructionAddress(Address, C_ADD);
  •   if ( Address == 0 ) return 0;

  •   Address = *(PULONG)(Address + 2);
  •   if (MmIsAddressValidEx((PVOID)Address) == VCS_INVALID) return 0;
遍历链表:


代码:
复制代码
  • typedef struct _KTIMER_TABLE_ENTRY_2K3 {
  •   LIST_ENTRY Entry;
  •   ULARGE_INTEGER Time;
  • }KTIMER_TABLE_ENTRY_2K3, *PKTIMER_TABLE_ENTRY_2K3;

  • #pragma PAGECODE
  • static ULONG GetDpcTimerInformation_2K3(PVOID* pvBuf)
  • {
  •   ULONG            NumberOfTimerTable;
  •   ULONG            i;
  •   ULONG            ulCount = 0;
  •   MyDpcTimer          MyDpc;
  •   PKDPCTIMER          pTimer = NULL;
  •   PKTIMER_TABLE_ENTRY_2K3    pTimerTableEntry = NULL;
  •   PLIST_ENTRY          pNextList = NULL;

  •   NumberOfTimerTable = 0x200;                                  //_KTIMER_TABLE_ENTRY数量
  •   pTimerTableEntry = (PKTIMER_TABLE_ENTRY_2K3)GetDpcTimerListHeadForXp_2K3();
  •   if (pTimerTableEntry == NULL) return 0;


  •   *pvBuf = CallExAllocatePoolWithTag(PagedPool,MAX_DPCTIMER_COUNT*sizeof(MyDpcTimer),64152);    //分配足够大的缓冲
  •   if (*pvBuf == NULL) return 0;
  •   RtlZeroMemory(*pvBuf,MAX_DPCTIMER_COUNT*sizeof(MyDpcTimer));



  •   for ( i = 0; i < NumberOfTimerTable; i++, pTimerTableEntry++ )                //保存在ntos中的一个结构数组
  •   {
  •     if (MmIsAddressValidEx((PVOID)pTimerTableEntry) == VCS_INVALID) goto __exit1;

  •     if (pTimerTableEntry->Time.HighPart == 0xFFFFFFFF) continue;              //为空的数组高位双字为FFFFFFFF


  •     if ( MmIsAddressValidEx((PVOID)pTimerTableEntry->Entry.Blink) == VCS_INVALID ) continue;  //如果listentry域地址无效,continue
  •     if ( MmIsAddressValidEx((PVOID)pTimerTableEntry->Entry.Flink) == VCS_INVALID ) continue;


  •     for ( pNextList   = (PLIST_ENTRY)pTimerTableEntry->Entry.Blink;
  •         pNextList  != (PLIST_ENTRY)&pTimerTableEntry->Entry;
  •         pNextList   = pNextList->Blink)
  •     {
  •       //取得timer对象
  •       pTimer = CONTAINING_RECORD(pNextList,KDPCTIMER,TimerListEntry);

  •       if (!MmIsAddressValid((PVOID)pTimer) ||
  •         !MmIsAddressValid((PVOID)pTimer->Dpc) ||
  •         !MmIsAddressValid((PVOID)pTimer->Dpc->DeferredRoutine))
  •       {
  •         if ( !MmIsAddressValid(pNextList->Blink) ) break;
  •         continue;
  •       }

  •       while (TRUE)
  •       {
  •         RtlZeroMemory(&MyDpc,sizeof(MyDpcTimer));
  •         if ( !MmIsAddressValid((PVOID)pTimer) ) break;
  •         MyDpc.TimerAddress = (ULONG)pTimer;

  •         if ( !MmIsAddressValid((PVOID)pTimer->Dpc) ) break;
  •         MyDpc.DpcAddress = (ULONG)pTimer->Dpc;

  •         if ( !MmIsAddressValid((PVOID)pTimer->Dpc->DeferredRoutine) ) break;
  •         MyDpc.DpcRoutineAddress = (ULONG)pTimer->Dpc->DeferredRoutine;

  •         if ( !MmIsAddressValid((PVOID)&pTimer->eriod) ) break;
  •         MyDpc.Period = pTimer->eriod;

  •         RtlMoveMemory( (PVOID)((ULONG)*pvBuf+ulCount*sizeof(MyDpcTimer)),&MyDpc,sizeof(MyDpcTimer) );
  •         ulCount++;
  •         if (ulCount >= MAX_DPCTIMER_COUNT) break;  

  •         break;
  •       }

  •       if ( !MmIsAddressValid(pNextList->Blink) ) break;

  •     }//end while
  •   }// end for

  •   return ulCount * sizeof(MyDpcTimer);

  • __exit1:
  •   if (*pvBuf != NULL) ExFreePool(*pvBuf);
  •   *pvBuf = NULL;
  •   return 0;
  • }
最后说一下 win7 下面的遍历,在 win7 下KiTimerTableListHead被从符号表中抹去了,所以也就没办法通过搜索的方式寻找了
在 win7 下定时器链表被放到了KPRCB结构中,在 win7 7600 和 win7 7601 中KPRCB+0x1960的位置:



图片:012.jpg
相关结构:



图片:013.jpg
win7下_KTIMER_TABLE_ENTRY数量又被调整为256,找到_KTIMER_TABLE.TimerEntries后,
遍历TimerEntries.Entry就行了
所以大体流程就是:取KPRCB地址,+0x1960偏移得到_KTIMER_TABLE,+0x40偏移得到_KTIMER_TABLE.TimerEntries
最后遍历_KTIMER_TABLE.TimerEntries.Entry

需要注意的是KPRCB是每个处理器都拥有一个的,所以需要遍历每一个处理器
KiProcessorBlock是一个未导出的全局变量,指向一个数组,这个数组保存了每一个CPU的KPRCB结构地址
而KiProcessorBlock在KdDebuggerData64这个结构中出现:



图片:014.jpg
KdDebuggerData64可以从KdVersionBlock后面得到,KdVersionBlock又是个啥啊?
http://hi.baidu.com/combojiang/blog/...e38012f28.html
combojiang大牛在这里有介绍,其中关于KdDebuggerData64结构的定义可以在wrk中找到 <搜索_KDDEBUGGER_DATA64>

遍历过程:
代码:

复制代码
  • #define MAX_PROCESSOR_COUNT 32
  • #pragma PAGECODE
  • static ULONG GetDpcTimerInformation_WIN7(PVOID* pvBuf)
  • {
  •   ULONG            TimerTableOffsetInKprcb;
  •   ULONG            NumberOfTimerTable;
  •   ULONG            NumberOfProcessor;
  •   ULONG            i,j;
  •   ULONG            ulTemp;
  •   ULONG            ulCount = 0;
  •   PULONG            pKiProcessorBlock = NULL;
  •   PKTIMER_TABLE_ENTRY_WIN7  pTimerTableEntryWin7 = NULL;
  •   PLIST_ENTRY          pNextList = NULL;
  •   PKDPCTIMER          pTimer = NULL;
  •   MyDpcTimer          MyDpc;

  •   TimerTableOffsetInKprcb = 0x1960+0x40;                            //首个_KTIMER_TABLE_ENTRY在PRCB中的偏移
  •   NumberOfTimerTable = 0x100;                                  //_KTIMER_TABLE_ENTRY数量

  •   NumberOfProcessor = (ULONG)KeNumberProcessors;                        //当前机器处理器数量
  •   if (NumberOfProcessor > MAX_PROCESSOR_COUNT) return 0;


  •   pKiProcessorBlock = (PULONG)GetKiProcessorBlock();                      //取得KiProcessorBlock,包含了NumberOfProcessor个KPRCB
  •   if (pKiProcessorBlock == NULL) return 0;

  •   
  •   *pvBuf = CallExAllocatePoolWithTag(PagedPool,MAX_DPCTIMER_COUNT*sizeof(MyDpcTimer),652);    //分配足够大的缓冲
  •   if (*pvBuf == NULL) return 0;
  •   RtlZeroMemory(*pvBuf,MAX_DPCTIMER_COUNT*sizeof(MyDpcTimer));


  •   for ( i = 0; i < NumberOfProcessor; i++, pKiProcessorBlock++ )                //DPC timer在每个cpu中都有一个队列,所以枚举每一个KPRCB
  •   {
  •     if ( MmIsAddressValidEx((PVOID)pKiProcessorBlock) == VCS_INVALID ) goto __exit1;    //检测一下当前的KPRCB地址是否可访问


  •     ulTemp = *pKiProcessorBlock + TimerTableOffsetInKprcb;                  //取得当前CPU的KPRCB中首个KTIMER_TABLE_ENTRY地址
  •     if ( MmIsAddressValidEx((PVOID)ulTemp) == VCS_INVALID )  goto __exit1;


  •     pTimerTableEntryWin7 = (PKTIMER_TABLE_ENTRY_WIN7)ulTemp;                //此时ulTemp是 timer table entry地址

  •     for ( j = 0; j < NumberOfTimerTable; j++, pTimerTableEntryWin7++ )            //准备遍历timer table表
  •     {
  •       if ( MmIsAddressValidEx((PVOID)pTimerTableEntryWin7) == VCS_INVALID ) goto __exit1;
  •       if ( pTimerTableEntryWin7->Time.HighPart == 0xFFFFFFFF ) continue;          //为空的数组高位双字为FFFFFFFF


  •       if ( MmIsAddressValidEx((PVOID)pTimerTableEntryWin7->Entry.Blink) == VCS_INVALID ) continue;
  •       if ( MmIsAddressValidEx((PVOID)pTimerTableEntryWin7->Entry.Flink) == VCS_INVALID ) continue;


  •       for ( pNextList   = (PLIST_ENTRY)pTimerTableEntryWin7->Entry.Blink;
  •           pNextList  != (PLIST_ENTRY)&pTimerTableEntryWin7->Entry;
  •           pNextList   = pNextList->Blink)
  •       {

  •         pTimer = CONTAINING_RECORD(pNextList,KDPCTIMER,TimerListEntry);          //取得timer对象

  •         if (!MmIsAddressValid((PVOID)pTimer)||
  •           !MmIsAddressValid((PVOID)pTimer->Dpc)||
  •           !MmIsAddressValid((PVOID)pTimer->Dpc->DeferredRoutine))
  •         {
  •           if (!MmIsAddressValid((PVOID)pNextList->Blink)) break;
  •           continue;
  •         }


  •         while (TRUE)
  •         {
  •           RtlZeroMemory(&MyDpc,sizeof(MyDpcTimer));
  •           if ( !MmIsAddressValid((PVOID)pTimer) ) break;
  •           MyDpc.TimerAddress = (ULONG)pTimer;

  •           if ( !MmIsAddressValid((PVOID)pTimer->Dpc) ) break;
  •           MyDpc.DpcAddress = (ULONG)pTimer->Dpc;

  •           if ( !MmIsAddressValid((PVOID)pTimer->Dpc->DeferredRoutine) ) break;
  •           MyDpc.DpcRoutineAddress = (ULONG)pTimer->Dpc->DeferredRoutine;

  •           if ( !MmIsAddressValid((PVOID)&pTimer->eriod) ) break;
  •           MyDpc.Period = pTimer->eriod;

  •           RtlMoveMemory( (PVOID)((ULONG)*pvBuf+ulCount*sizeof(MyDpcTimer)),&MyDpc,sizeof(MyDpcTimer) );
  •           ulCount++;
  •           if (ulCount >= MAX_DPCTIMER_COUNT) break;  

  •           break;
  •         }

  •         if (!MmIsAddressValid((PVOID)pNextList->Blink)) break;

  •       }// end while
  •     }// end for
  •   }// end for

  •   return ulCount * sizeof(MyDpcTimer);

  • __exit1:
  •   if (*pvBuf != NULL) ExFreePool(*pvBuf);
  •   *pvBuf = NULL;
  •   return 0;
  • }

大体过程与 2k3是一样的,可以修改一下以兼容两个系统

最后看一下 win7 下的枚举效果:



图片:015.jpg



图片:016.jpg
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

果子博客
扫码关注微信公众号

Archiver|手机版|小黑屋|风叶林

GMT+8, 2026-2-1 04:43 , Processed in 0.073054 second(s), 20 queries .

Powered by 风叶林

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表