首页
社区
课程
招聘
[原创]多处理器下Windows内核同步
2008-12-5 11:33 16760

[原创]多处理器下Windows内核同步

2008-12-5 11:33
16760
  同步问题在安全领域用的最多的地方无疑是在做Inline-HOOK时。
  一直以来,包括现在,很多程序都在使用:
  KeInitializeSpinLock
  KeAcquireSpinLock
  KeReleaseSpinLock
  来进行同步处理,调用这个自旋锁在会提升IRQL到DPC级别来屏蔽线程切换,在单核下这是有效的,这点大家是有共识的。
  
  但在多核下,DPC的提升只对当前处理器有效,所以仅IRQL已无法屏蔽另一处理器上的线程调度,也就是说在你的HOOK进行一半时很可能被另一个线程打断而出错。
  
  比如,你在安装HOOK时,需要写一句代码:JMP XXXXXXX 当刚写入JMP还没来得及写入XXXXXXX时,另一个处理器中的另一个线程已经开始执行这一句代码,那结果可想而知。
  
  那么
  KeInitializeSpinLock
  KeAcquireSpinLock
  KeReleaseSpinLock
  在这种多核情况下可以起到作用么?看看这个都做了哪些工作就了解了。
  
  nt!KeInitializeSpinLock:        ;仅仅是把锁结构清零
  80541970 8b442404 mov eax,dword ptr [esp+4]
  80541974 c70000000000 mov dword ptr [eax],0
  8054197a c20400 ret 4
  
  而KeAcquireSpinLock最终会调用hal!KfAcquireSpinLock,反汇编代码如下:
  
  
  hal!KfAcquireSpinLock:        ;先提升IRQL级别
  806e7830 8b158000feff mov edx,dword ptr ds:[0FFFE0080h]
  806e7836 c7058000feff41000000 mov dword ptr ds:[0FFFE0080h],41h
  806e7840 c1ea04 shr edx,4
  806e7843 0fb68298226f80 movzx eax,byte ptr hal!HalpVectorToIRQL (806f2298)[edx]
  
  806e784a f00fba2900 lock bts dword ptr [ecx],0 ;ECX中是锁结构,以原子操作指令判断是不是零,如果是零则置位成1(即上锁)如果是1则执行Pause后再JMP回去循环测试。
  806e784f 7203 jb hal!KfAcquireSpinLock+0x24 (806e7854)
  806e7851 c3 ret
  806e7854 f70101000000 test dword ptr [ecx],1
  806e785a 74ee je hal!KfAcquireSpinLock+0x1a (806e784a)
  806e785c f390 pause
  806e785e ebf4 jmp hal!KfAcquireSpinLock+0x24 (806e7854)
  
  由以上代码来看:
  KeInitializeSpinLock
  KeAcquireSpinLock
  KeReleaseSpinLock
  对其它处理器上的其它线程是无效的,除非那个线程用的是跟你同一个自旋锁,也就是KeInitializeSpinLock的是同一个地址。
  
  结论有了,但方法呢?究竟怎么才能实现多核同步呢?
  现在流行一种方法,那就是多CPU同时提升IRQL到DPC,以实现多核下的同步,这也有人在用了,自己的HOOK线程工作在一个CPU,让其它CPU在DPC级别空转,直到HOOK操作完成。
  理论上这种方法是可行的,实际中也是可行的,但稳定性有待考验。
  
  而我却一直有个疑问,Windows难道就没有提供现成的多核同步机制吗?难道Windows本身就不需要多核同步吗?显然不是的,Windows也需要同步。
  翻了一些资料,逆了一些Windows代码后,找到了一种我认为是系统自用的多核同步方式,经初步试验是有一定效果的,至于是否能完全实现同步,还需进一步验证,欢迎大家测试。
  
  在说之前,先看一段源码:
  
  
  
  VOID
  NTAPI
  KiInitSpinLocks(IN PKPRCB Prcb,
   IN CCHAR Number)
  {
   ULONG i;
  
   /* Initialize Dispatcher Fields */
   Prcb->QueueIndex = 1;
   Prcb->ReadySummary = 0;
   Prcb->DeferredReadyListHead.Next = NULL;
   for (i = 0; i < MAXIMUM_PRIORITY; i++)
   {
   /* Initialize the ready list */
   InitializeListHead(&Prcb->DispatcherReadyListHead[i]);
   }
  
   /* Initialize DPC Fields */
   InitializeListHead(&Prcb->DpcData[DPC_NORMAL].DpcListHead);
   KeInitializeSpinLock(&Prcb->DpcData[DPC_NORMAL].DpcLock);
   Prcb->DpcData[DPC_NORMAL].DpcQueueDepth = 0;
   Prcb->DpcData[DPC_NORMAL].DpcCount = 0;
   Prcb->DpcRoutineActive = FALSE;
   Prcb->MaximumDpcQueueDepth = KiMaximumDpcQueueDepth;
   Prcb->MinimumDpcRate = KiMinimumDpcRate;
   Prcb->AdjustDpcThreshold = KiAdjustDpcThreshold;
   KeInitializeDpc(&Prcb->CallDpc, NULL, NULL);
   KeSetTargetProcessorDpc(&Prcb->CallDpc, Number);
   KeSetImportanceDpc(&Prcb->CallDpc, HighImportance);
  
   /* Initialize the Wait List Head */
   InitializeListHead(&Prcb->WaitListHead);
  
   /* Initialize Queued Spinlocks */
   Prcb->LockQueue[LockQueueDispatcherLock].Next = NULL;
   Prcb->LockQueue[LockQueueDispatcherLock].Lock = &KiDispatcherLock;
   Prcb->LockQueue[LockQueueExpansionLock].Next = NULL;
   Prcb->LockQueue[LockQueueExpansionLock].Lock = NULL;
   Prcb->LockQueue[LockQueuePfnLock].Next = NULL;
   Prcb->LockQueue[LockQueuePfnLock].Lock = &MmPfnLock;
   Prcb->LockQueue[LockQueueSystemSpaceLock].Next = NULL;
   Prcb->LockQueue[LockQueueSystemSpaceLock].Lock = &MmSystemSpaceLock;
   Prcb->LockQueue[LockQueueBcbLock].Next = NULL;
   Prcb->LockQueue[LockQueueBcbLock].Lock = &CcBcbSpinLock;
   Prcb->LockQueue[LockQueueMasterLock].Next = NULL;
   Prcb->LockQueue[LockQueueMasterLock].Lock = &CcMasterSpinLock;
   Prcb->LockQueue[LockQueueVacbLock].Next = NULL;
   Prcb->LockQueue[LockQueueVacbLock].Lock = &CcVacbSpinLock;
   Prcb->LockQueue[LockQueueWorkQueueLock].Next = NULL;
   Prcb->LockQueue[LockQueueWorkQueueLock].Lock = &CcWorkQueueSpinLock;
   Prcb->LockQueue[LockQueueNonPagedPoolLock].Next = NULL;
   Prcb->LockQueue[LockQueueNonPagedPoolLock].Lock = &NonPagedPoolLock;
   Prcb->LockQueue[LockQueueMmNonPagedPoolLock].Next = NULL;
   Prcb->LockQueue[LockQueueMmNonPagedPoolLock].Lock = &MmNonPagedPoolLock;
   Prcb->LockQueue[LockQueueIoCancelLock].Next = NULL;
   Prcb->LockQueue[LockQueueIoCancelLock].Lock = &IopCancelSpinLock;
   Prcb->LockQueue[LockQueueIoVpbLock].Next = NULL;
   Prcb->LockQueue[LockQueueIoVpbLock].Lock = &IopVpbSpinLock;
   Prcb->LockQueue[LockQueueIoDatabaseLock].Next = NULL;
   Prcb->LockQueue[LockQueueIoDatabaseLock].Lock = &IopDatabaseLock;
   Prcb->LockQueue[LockQueueIoCompletionLock].Next = NULL;
   Prcb->LockQueue[LockQueueIoCompletionLock].Lock = &IopCompletionLock;
   Prcb->LockQueue[LockQueueNtfsStructLock].Next = NULL;
   Prcb->LockQueue[LockQueueNtfsStructLock].Lock = &NtfsStructLock;
   Prcb->LockQueue[LockQueueAfdWorkQueueLock].Next = NULL;
   Prcb->LockQueue[LockQueueAfdWorkQueueLock].Lock = &AfdWorkQueueSpinLock;
   Prcb->LockQueue[LockQueueUnusedSpare16].Next = NULL;
   Prcb->LockQueue[LockQueueUnusedSpare16].Lock = NULL;
  
   /* Loop timer locks */
   for (i = 0; i < LOCK_QUEUE_TIMER_TABLE_LOCKS; i++)
   {
   /* Initialize the lock and setup the Queued Spinlock */
   KeInitializeSpinLock(&KiTimerTableLock[i]);
   Prcb->LockQueue[LockQueueTimerTableLock + i].Next = NULL;
   Prcb->LockQueue[LockQueueTimerTableLock + i].Lock =
   &KiTimerTableLock[i];
   }
  
   /* Initialize the PRCB lock */
   KeInitializeSpinLock(&Prcb->PrcbLock);
  
   /* Check if this is the boot CPU */
   if (!Number)
   {
   /* Initialize the lock themselves */
   KeInitializeSpinLock(&KiDispatcherLock);
   KeInitializeSpinLock(&KiReverseStallIpiLock);
   KeInitializeSpinLock(&MmPfnLock);
   KeInitializeSpinLock(&MmSystemSpaceLock);
   KeInitializeSpinLock(&CcBcbSpinLock);
   KeInitializeSpinLock(&CcMasterSpinLock);
   KeInitializeSpinLock(&CcVacbSpinLock);
   KeInitializeSpinLock(&CcWorkQueueSpinLock);
   KeInitializeSpinLock(&IopCancelSpinLock);
   KeInitializeSpinLock(&IopCompletionLock);
   KeInitializeSpinLock(&IopDatabaseLock);
   KeInitializeSpinLock(&IopVpbSpinLock);
   KeInitializeSpinLock(&NonPagedPoolLock);
   KeInitializeSpinLock(&MmNonPagedPoolLock);
   KeInitializeSpinLock(&NtfsStructLock);
   KeInitializeSpinLock(&AfdWorkQueueSpinLock);
   }
  }
  
  这是系统启动阶段初始化锁时的一段代码,在这里系统初始化了一些自己需要的锁,而我们需要关心的是这个锁:
  KiDispatcherLock
  这个“分发器自旋锁”,当系统进行线程分发时会偿试获取这个锁。
  而锁住这个也就可以屏蔽系统的线程分发,理论上好像也就不会有线程去打断你的执行了。(个人理解,不一定对,没有文档这么说过)
  
  系统在进行线程级操作时,的确也用到了这个锁,我们看看暂停线程时用锁的情况:
  
  nt!KeSuspendThread:
  804fe4e8 8bff mov edi,edi
  804fe4ea 55 push ebp
  804fe4eb 8bec mov ebp,esp
  804fe4ed 83ec0c sub esp,0Ch
  804fe4f0 53 push ebx
  804fe4f1 56 push esi
  804fe4f2 8b7508 mov esi,dword ptr [ebp+8]
  804fe4f5 57 push edi
  804fe4f6 8d8ee8000000 lea ecx,[esi+0E8h]
  804fe4fc 8d55f4 lea edx,[ebp-0Ch]
  804fe4ff ff152c914d80 call dword ptr [nt!_imp_KeAcquireInStackQueuedSpinLockRaiseToSynch (804d912c)]
  804fe505 64a120000000 mov eax,dword ptr fs:[00000020h]
  804fe50b 8d8818040000 lea ecx,[eax+418h]
  804fe511 e86a350400 call nt!KeAcquireQueuedSpinLockAtDpcLevel (80541a80)
  804fe516 0fbebeb9010000 movsx edi,byte ptr [esi+1B9h]
  804fe51d 83ff7f cmp edi,7Fh
  804fe520 8b1d14914d80 mov ebx,dword ptr [nt!_imp_KeReleaseInStackQueuedSpinLock (804d9114)]
  804fe526 7520 jne nt!KeSuspendThread+0x60 (804fe548)
  804fe528 64a120000000 mov eax,dword ptr fs:[00000020h]
  804fe52e 8d8818040000 lea ecx,[eax+418h]
  804fe534 e873350400 call nt!KeReleaseQueuedSpinLockFromDpcLevel (80541aac)
  
  暂停线程时,系统使用了这个锁,先调用nt!KeAcquireQueuedSpinLockAtDpcLevel上锁,后调用nt!KeReleaseQueuedSpinLockFromDpcLevel解锁。
  
  看代码:
  nt!KeAcquireQueuedSpinLockAtDpcLevel:
  80541a80 8b5104 mov edx,dword ptr [ecx+4]
  1: kd> r
  eax=f9c2c120 ebx=8187fe18 ecx=f9c2c538 edx=00000000 esi=8187fda8 edi=00000000
  eip=80541a80 esp=f795c988 ebp=f795c9bc iopl=0 nv up ei ng nz ac po cy
  cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000293
  nt!KeAcquireQueuedSpinLockAtDpcLevel:
  80541a80 8b5104 mov edx,dword ptr [ecx+4] ds:0023:f9c2c53c={nt!KiDispatcherLock (80554880)}
  
  注意:ECX+4里面就是KiDispatcherLock线程分发自旋锁了。
  
  这个ECX是哪里来的呢?找不到KiDispatcherLock的指针,说什么都没用,因为我们的理念是与系统共用一个锁来禁止在我们操作期间产生线程切换,所以锁的指针是必须的。
  其实获取也很简单:
  804fe528 64a120000000 mov eax,dword ptr fs:[00000020h]
  804fe52e 8d8818040000 lea ecx,[eax+418h]
  这里就是了!
  
  但需要说明的是,这个锁的使用,并非简单的置位1或0,这里我模拟Windows写了两段上锁解锁代码,供大家使用,简单的模拟实现,没做什么优化,大家凑合着用吧:
  
  ;=====================================================
  ;多核同步锁
  ;=====================================================
  _LockCPU        proc
         
           assume fs:nothing
           mov eax,dword ptr fs:[00000020h]
           add eax,420h
           mov ecx,eax
           mov edx,dword ptr [ecx+4]
           mov eax,ecx
           xchg eax,dword ptr [edx]
           cmp eax,0
           jne StartTest
           or edx,2
           mov dword ptr [ecx+4],edx
          ExitLock:
           ret
  
          StartTest:
           or edx,1
           mov dword ptr [ecx+4],edx
           mov dword ptr [eax],ecx
          LoopTest:
           test dword ptr [ecx+4],1
           je ExitLock
           db        0f3h,90h
           jmp LoopTest
         
  _LockCPU endp
  ;;------------------------------
  _UnLockCPU        proc
         
           assume fs:nothing
           mov eax,dword ptr fs:[00000020h]
           add eax,420h
           mov        ecx,eax
         
           mov edx,dword ptr [ecx]
           mov ecx,dword ptr [ecx+4]
           test edx,edx
           btr ecx,1
           mov dword ptr [eax+4],ecx
           jne UnExit
         
           xor edx,edx
           push eax
           lock cmpxchg dword ptr [ecx],edx
           pop eax
           jne UnLoopTest
           ret
  
          UnExit:
           xor dword ptr [edx+4],3
           mov dword ptr [eax],0
           ret
  
          UnLoopTest:
           mov edx,dword ptr [eax]
           test edx,edx
           jne UnExit
         
           db        0f3h,90h
           jmp UnLoopTest
  
  _UnLockCPU endp
  
  我做了一个测试,InlineHOOK了调用相对频繁的KeInsertQueueApc,将HOOK与UnHOOK代码放到了一个定时器中,每秒种HOOK与UnHOOK一次,仅仅用上面两段代码加锁,没加任何其它措施,比如提升IRQL等,在多核处理器中跑了很长时间后,没发现问题。
  当然了,这并不能说明就没有问题,谁用谁测试吧,有什么问题欢迎反馈。
       http://musehero.blog.tianya.cn。
  

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

收藏
点赞8
打赏
分享
最新回复 (20)
雪    币: 331
活跃值: (57)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
better 2 2008-12-5 12:55
2
0
同步问题在安全领域用的最多的地方无疑是在做Inline-HOOK时

赞一个……

我记得多CPU的自旋锁不是通过提升IRQL来实现的,
windows把双核看成是多CPU……
雪    币: 709
活跃值: (2230)
能力值: ( LV12,RANK:1010 )
在线值:
发帖
回帖
粉丝
sudami 25 2008-12-5 16:15
3
0
顶顶
~~??
雪    币: 7300
活跃值: (3758)
能力值: (RANK:1130 )
在线值:
发帖
回帖
粉丝
海风月影 22 2008-12-5 17:04
4
0
狙      剑
雪    币: 635
活跃值: (101)
能力值: ( LV12,RANK:420 )
在线值:
发帖
回帖
粉丝
qihoocom 9 2008-12-5 19:29
5
0
没啥用

用来hook? 多指令HOOK必然不安全,单指令HOOK ExInterLockedXXX解决

找偏移不怎么稳定
雪    币: 248
活跃值: (26)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
MuseHero 1 2008-12-5 23:25
6
0
呵,老大没看仔细 ,我没说多核下仅仅是提升IRQL ,Windows还会Test and Set 锁。
看这个:

--------------------------
KeInitializeSpinLock
  KeAcquireSpinLock
  KeReleaseSpinLock
  在这种多核情况下可以起到作用么?看看这个都做了哪些工作就了解了。
  
  nt!KeInitializeSpinLock:  ;仅仅是把锁结构清零
  80541970 8b442404 mov eax,dword ptr [esp+4]
  80541974 c70000000000 mov dword ptr [eax],0
  8054197a c20400 ret 4
  
  而KeAcquireSpinLock最终会调用hal!KfAcquireSpinLock,反汇编代码如下:
  
  
  hal!KfAcquireSpinLock:  ;先提升IRQL级别
  806e7830 8b158000feff mov edx,dword ptr ds:[0FFFE0080h]
  806e7836 c7058000feff41000000 mov dword ptr ds:[0FFFE0080h],41h
  806e7840 c1ea04 shr edx,4
  806e7843 0fb68298226f80 movzx eax,byte ptr hal!HalpVectorToIRQL (806f2298)[edx]
  
  806e784a f00fba2900 lock bts dword ptr [ecx],0 ;ECX中是锁结构,以原子操作指令判断是不是零,如果是零则置位成1(即上锁)如果是1则执行Pause后再JMP回去循环测试。
  806e784f 7203 jb hal!KfAcquireSpinLock+0x24 (806e7854)
  806e7851 c3 ret
  806e7854 f70101000000 test dword ptr [ecx],1
  806e785a 74ee je hal!KfAcquireSpinLock+0x1a (806e784a)
  806e785c f390 pause
  806e785e ebf4 jmp hal!KfAcquireSpinLock+0x24 (806e7854)
  
  由以上代码来看:
  KeInitializeSpinLock
  KeAcquireSpinLock
  KeReleaseSpinLock
  对其它处理器上的其它线程是无效的,除非那个线程用的是跟你同一个自旋锁,也就是KeInitializeSpinLock的是同一个地址。
  
雪    币: 231
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
yyccaa 2008-12-5 23:35
7
0
普通自旋锁的真正目的是用于共享数据的保护,而非运行代码的保护,所以普通自旋锁的粒度是较细的,它不会阻止其它cpu进行任务调度。
但如果要对代码做保护,那就必须阻止所有cpu上的任务调度(任务调度部分需要获得该锁,以同步处理任务链表),在linux的内核从2.0到2.2过渡时就提供了类似的自旋锁叫BKL(大内核锁)。
但这种锁不该长时间持有,多个cpu一块空转是严重浪费资源的。
雪    币: 331
活跃值: (57)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
better 2 2008-12-6 00:11
8
0
明白楼主的意思了,是一个隐患……
多CPU同时提升IRQL到DPC,以实现多核下的同步

貌似也不错嘛……呵呵
雪    币: 231
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
yyccaa 2008-12-6 13:15
9
0
这不是隐患,而是违背了自旋锁的使用目的。楼主现在通过这个方法其实还是以抢夺内核任务调度模块所需的锁(这个锁也是为了保护任务链表,而不是代码设计的),来停止任务调度(这目的和所有处理器都运行于DPC上是类似的)。
雪    币: 66
活跃值: (16)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
炉子 3 2008-12-6 13:57
10
0
          
雪    币: 248
活跃值: (26)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
MuseHero 1 2008-12-7 00:32
11
0
其实,hal导出了两个实现函数:
        fastcall KeAcquireQueuedSpinLock
        fastcall KeReleaseQueuedSpinLock
可以不用自己写的

yyccaa说的没错,长时间占用这个锁会出问题的,所以小技巧的应用还是不能少的。
这类东西风险还是挺大的,包括全CPU提升DPC也存在风险,长时间占用仍然会完蛋,在关键操作前调用ZwYieldExecution是好习惯~最好在上锁之前,调一次ZwYieldExecution,来保证后面可以得到一个完整的时间片,尽量减少对系统线程调度的影响。
雪    币: 8861
活跃值: (2369)
能力值: ( LV12,RANK:760 )
在线值:
发帖
回帖
粉丝
cvcvxk 10 2008-12-7 19:21
12
0
据说调用ZwYieldExecution也存在一些问题~

2核,4核都挺和谐的~
3核很不和谐~~~
雪    币: 227
活跃值: (13)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
haifengjl 2009-7-9 13:07
13
0
顶!!!顶!!!
雪    币: 563
活跃值: (95)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lixupeng 2009-7-9 16:50
14
0
天书啊 收下了 日后研究
雪    币: 284
活跃值: (16)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
jerrynpc 2009-7-10 09:09
15
0
TITLE : 每个人 or 每个机器 diffrent,我只是路过来看看大牛们都在说什么。呵呵
雪    币: 367
活跃值: (20)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
morning 1 2009-7-12 22:55
16
0
不错,顶一下LZ
雪    币: 239
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
fdltc 2009-7-18 03:25
17
0
使用汇编优化hook指令,可以精简到两个原子指令

提升irql,强行冲关即可

何必这么麻烦
雪    币: 724
活跃值: (81)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
半道出家 2009-7-18 22:05
18
0
这是问题的根本, 自旋锁的目的是资源访问序列化,线程调度被屏蔽,并不意味着CPU的运行被挂起,不能解决
比如,你在安装HOOK时,需要写一句代码:JMP XXXXXXX 当刚写入JMP还没来得及写入XXXXXXX时,另一个处理器中的另一个线程已经开始执行这一句代码,那结果可想而知。
这样的问题。因为当你通过自旋锁屏蔽线程调度后继续你的HOOK进程时,其它内核的也仍在继续运行,仍然可能发生楼主描述的问题,而且,DPC仍然也在分发,DPC也可能访问HOOK的代码。对于INLINE HOOK,先做好所有准备工作,如果一定要修改待HOOK函数的前5个字,最安全的办法是关中断的情况下,用连续两个内存写完成,先写一个DWORD,再写一个BYTE,这样就可以了。
雪    币: 269
活跃值: (25)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
ReturnsMe 2 2009-8-13 07:40
19
0
顶~不过个人还是觉得raise IRQL比较省心....
雪    币: 101
活跃值: (21)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
jdzfjfhnui 2009-9-1 16:08
20
0
thanks
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
first农夫 2009-9-1 16:57
21
0
这个的确不错啊,虽然有点小隐患。
游客
登录 | 注册 方可回帖
返回