欧美vvv,亚洲第一成人在线,亚洲成人欧美日韩在线观看,日本猛少妇猛色XXXXX猛叫

新聞資訊

    根據前面的介紹,NT內核會把操作系統的代碼和數據映射到系統中所有進程的內核空間中。這樣,每個進程內的應用程序代碼便可以很方便地調用內核空間中的系統服務。這里的“很方便”有多層含義,一方面是內核代碼和用戶代碼在一個地址空間中,應用程序調用系統服務時不需要切換地址空間,另一方面是整個系統中內核空間的地址是統一的,編寫內核空間的代碼時會簡單很多。但是,如此設計也帶來一個很大的問題,那就是用戶空間中的程序指針可以指向內核空間中的數據和代碼,因此必須防止用戶代碼破壞內核空間中的操作系統。怎么做呢?答案是利用權限控制來實現對內核空間的保護。

    2.6.1 訪問模式

    Windows定義了兩種訪問模式(access mode)——用戶模式(user mode,也稱為用戶態)和內核模式(kernel mode,也稱為內核態)。應用程序(代碼)運行在用戶模式下,操作系統代碼運行在內核模式下。內核模式對應于處理器的最高權限級別(不考慮虛擬機情況),在內核模式下執行的代碼可以訪問所有系統資源并具有使用所有特權指令的權利。相對而言,用戶模式對應于較低的處理器優先級,在用戶模式下執行的代碼只可以訪問系統允許其訪問的內存空間,并且沒有使用特權指令的權利。

    本書卷1介紹過,IA-32處理器定義了4種特權級別(privilege level),或者稱為環(ring),分別為0、1、2、3,優先級0(環0)的特權級別最高。處理器在硬件一級保證高優先級的數據和代碼不會被低優先級的代碼破壞。Windows系統使用了IA-32處理器所定義的4種優先級中的兩種,優先級3(環3)用于用戶模式,優先級0用于內核模式。之所以只使用了其中的兩種,主要是因為有些處理器只支持兩種優先級,比如Compaq Alpha處理器。值得說明的是,對于x86處理器來說,并沒有任何寄存器表明處理器當前處于何種模式(或優先級)下,優先級只是代碼或數據所在的內存段或頁的一個屬性,參見卷1的2.6節和2.7節。

    因為內核模式下的數據和代碼具有較高的優先級,所以用戶模式下的代碼不可以直接訪問內核空間中的數據,也不可以直接調用內核空間中的任何函數或例程。任何這樣的嘗試都會導致保護性錯誤。也就是說,即使用戶空間中的代碼指針正確指向了要訪問的數據或代碼,但一旦訪問發生,那么處理器會檢測到該訪問是違法的,會停止該訪問并產生保護性異常(#GP)。

    雖然不可以直接訪問,但是用戶程序可以通過調用系統服務來間接訪問內核空間中的數據或間接調用、執行內核空間中的代碼。當調用系統服務時,主調線程會從用戶模式切換到內核模式,調用結束后再返回到用戶模式,也就是所謂的模式切換。在線程的KTHREAD結構中,定義了UserTime和KernelTime兩個字段,分別用來記錄這個線程在用戶模式和內核模式的運行時間(以時鐘中斷次數為單位)。模式切換是通過軟中斷或專門的快速系統調用(fast system call)指令來實現的。下面通過一個例子來分別介紹這兩種切換機制。

    2.6.2 使用INT 2E切換到內核模式

    圖2-4展示了在Windows 2000中通過INT 2E從應用程序調用ReadFile() API的過程。因為ReadFile() API是從Kernel32.dll導出的,所以我們看到該調用首先轉到Kernel32.dll中的ReadFile()函數,ReadFile()函數在對參數進行簡單檢查后便調用NtDll.dll中的NtReadFile()函數。

    圖2-4 通過INT 2E從應用程序調用ReadFile() API的過程

    通過反匯編可以看到,NtDll.dll中的NtReadFile ()函數非常簡短,首先將ReadFile()對應的系統服務號(0xa1,與版本有關)放入EAX寄存器中,將參數指針放入EDX寄存器中,然后便通過INT n指令發出調用。這里要說明的一點是,雖然每個系統服務都具有唯一的號碼,但微軟公司沒有公開這些服務號,也不保證這些號碼在不同的Windows版本中會保持一致。

    ntdll!NtReadFile: // Windows 2000
    77f8fb5d b8a1000000    mov      eax,0xa1
    77f8fb62 8d542404      lea      edx,[esp+0x4]
    77f8fb66 cd2e          int      2e
    77f8fb68 c22400        ret      0x24

    在WinDBG下通過!idt 2e命令可以看到2e號向量對應的服務例程是KiSystemService ()KiSystemService ()是內核態中專門用來分發系統調用的例程。

    lkd> !idt 2e
    Dumping IDT:
    2e:   804db1ed nt!KiSystemService

    Windows將2e號向量專門用于系統調用,在啟動早期初始化中斷描述符表(Interrupt Descriptor Table,IDT)時(見第11章)便注冊好了合適的服務例程。因此當NTDll.DLL中的NtReadFile()發出INT 2E指令后,CPU便會通過IDT找到KiSystemService ()函數。因為KiSystemService ()函數是位于內核空間的,所以CPU在把執行權交給KiSystemService ()函數前,會做好從用戶模式切換到內核模式的各種工作,包括:

    (1)權限檢查,即檢查源位置和目標位置所在的代碼段權限,核實是否可以轉移;

    (2)準備內核模式使用的棧,為了保證內核安全,所有線程在內核態執行時都必須使用位于內核空間的內核棧(kernel stack),內核棧的大小一般為8KB或12KB。

    KiSystemService ()會根據服務ID從系統服務分發表(System Service Dispatch Table)中查找到要調用的服務函數地址和參數描述,然后將參數從用戶態棧復制到該線程的內核棧中,最后KiSystemService ()調用內核中真正的NtReadFile()函數,執行讀文件的操作,操作結束后會返回到KiSystemService ()KiSystemService ()會將操作結果復制回該線程用戶態棧,最后通過IRET指令將執行權交回給NtDll.dll中的NtReadFile()函數(繼續執行INT 2E后面的那條指令)。

    通過INT 2E進行系統調用時,CPU必須從內存中分別加載門描述符和段描述符才能得到KiSystemService ()的地址,即使門描述符和段描述符已經在高速緩存中,CPU也需要通過“內存讀(memory read)”操作從高速緩存中讀出這些數據,然后進行權限檢查。

    2.6.3 快速系統調用

    因為系統調用是非常頻繁的操作,所以如果能減少這些開銷還是非常有意義的。可以從兩個方面來降低開銷:一是把系統調用服務例程的地址放到寄存器中以避免讀IDT這樣的內存操作,因為讀寄存器的速度比讀內存的速度要快很多;二是避免權限檢查,也就是使用特殊的指令讓CPU省去那些對系統服務調用來說根本不需要的權限檢查。奔騰II處理器引入的SYSENTER/SYSEXIT指令正是按這一思路設計的。AMD K7引入的SYSCALL/SYSRETURN指令也是為這一目的而設計的。相對于INT 2E,使用這些指令可以加快系統調用的速度,因此利用這些指令進行的系統調用稱為快速系統調用。

    下面我們介紹Windows系統是如何利用IA-32處理器的SYSENTER/SYSEXIT指令(從奔騰II開始)實現快速系統調用的[2]。首先,Windows 2000或之前的Windows系統不支持快速系統調用,它們只能使用前面介紹的INT 2E方式進行系統調用。Windows XP和Windows Server 2003或更新的版本在啟動過程中會通過CPUID指令檢測CPU是否支持快速系統調用指令(EDX寄存器的SEP標志位)。如果CPU不支持這些指令,那么仍使用INT 2E方式。如果CPU支持這些指令,那么Windows系統便會決定使用新的方式進行系統調用,并做好如下準備工作。

    (1)在全局描述符表(GDT)中建立4個段描述符,分別用來描述供SYSENTER指令進入內核模式時使用的代碼段(CS)和棧段(SS),以及SYSEXIT指令從內核模式返回用戶模式時使用的代碼段和棧段。這4個段描述符在GDT中的排列應該嚴格按照以上順序,只要指定一個段描述符的位置便能計算出其他的。

    (2)設置表2-1中專門用于系統調用的MSR(關于MSR的詳細介紹見卷1的2.4.3節),SYSENTER_EIP_MSR用于指定新的程序指針,也就是SYSENTER指令要跳轉到的目標例程地址。Windows系統會將其設置為KiFastCallEntry的地址,因為KiFastCallEntry例程是Windows內核中專門用來受理快速系統調用的。SYSENTER_CS_MSR用來指定新的代碼段,也就是KiFastCallEntry所在的代碼段。SYSENTER_ESP_MSR用于指定新的棧指針(ESP)。新的棧段是由SYSENTER_CS_MSR的值加8得來的。

    (3)將一小段名為SystemCallStub的代碼復制到SharedUserData內存區,該內存區會被映射到每個Win32進程的進程空間中。這樣當應用程序每次進行系統調用時,NTDll.DLL中的殘根(stub)函數便調用這段SystemCallStub代碼。SystemCallStub的內容因系統硬件的不同而不同,對于IA-32處理器,該代碼使用SYSENTER指令,對于AMD處理器,該代碼使用SYSCALL指令。

    表2-1 供SYSENTER指令使用的MSR(略)

    例如在配有Pentium M CPU的Windows XP系統上,以上3個寄存器的值分別為:

    lkd> rdmsr 174
    msr[174]=00000000`00000008
    lkd> rdmsr 175
    msr[175]=00000000`bacd8000
    lkd> rdmsr 176
    msr[176]=00000000`8053cad0

    其中SYSENTER_CS_MSR的值為8,這是Windows系統的內核代碼段的選擇子,即常量KGDT_R0_CODE的值。WinDBG幫助文件中關于dg命令的說明中列出了這個常量。SYSENTER_EIP_MSR的值是8053cad0,檢查nt內核中KiFastCallEntry函數的地址。

    lkd> x nt!KiFastCallEntry
    8053cad0 nt!KiFastCallEntry=<no type information>

    可見,Windows把快速系統調用的目標指向內核代碼段中的KiFastCallEntry函數。

    通過反匯編Windows XP下NTDll.DLL中的NtReadFile ()函數,可以看到SystemCallStub被映射到進程的0x7ffe0300位置。與前面Windows 2000下的版本相比,容易看到該服務的系統服務號碼在這兩個版本間是不同的。

    kd> u ntdll...
    ntdll!NtReadFile: // Windows XP
    77f5bfa8 b8b7000000       mov     eax,0xb7
    77f5bfad ba0003fe7f       mov     edx,0x7ffe0300
    77f5bfb2 ffd2             call edx {SharedUserData!SystemCallStub (7ffe0300)}
    77f5bfb4 c22400           ret     0x24
    77f5bfb7 90               nop

    觀察本段下面反匯編SystemCallStub的結果,它只包含3條指令,分別用于將棧指針(ESP寄存器)放入EDX寄存器中、執行sysenter指令和返回。第一條指令有兩個用途:一是向內核空間傳遞參數;二是指定從內核模式返回時的棧地址。因為筆者使用的是英特爾奔騰M處理器,所以此處是sysenter指令,對于AMD處理器,此處應該是syscall指令。

    kd> u...
    SharedUserData!SystemCallStub:
    7ffe0300 8bd4             mov     edx,esp
    7ffe0302 0f34             sysenter
    7ffe0304 c3               ret

    下面讓我們看一下KiFastCallEntry例程,其清單如下所示。

    kd> u nt!KiFastCallEntry L20
    nt!KiFastCallEntry:
    804db1bb 368b0d40f0dfff   mov      ecx,ss:[ffdff040]
    804db1c2 368b6104         mov      esp,ss:[ecx+0x4]
    804db1c6 b90403fe7f       mov      ecx,0x7ffe0304
    804db1cb 3b2504f0dfff     cmp      esp,[ffdff004]
    804db1d1 0f84cc030000     je       nt!KiServiceExit2+0x13f (804db5a3)
    804db1d7 6a23             push     0x23
    804db1d9 52               push     edx
    804db1da 83c208           add      edx,0x8
    804db1dd 6802020000       push     0x202
    804db1e2 6a02             push     0x2
    804db1e4 9d               popfd
    804db1e5 6a1b             push     0x1b
    804db1e7 51               push     ecx // Fall Through,自然進入KiSystemService函數
    nt!KiSystemService:
    804db1e8 90               nop
    804db1e9 90               nop
    804db1ea 90               nop
    804db1eb 90               nop
    804db1ec 90                nop
    nt!KiSystemService:
    804db1ed 6a00             push      0x0
    804db1ef 55               push      ebp

    顯而易見,KiFastCallEntry在做了些簡單操作后,便下落(fall through)到KiSystemService函數了,也就是說,快速系統調用和使用INT 2E進行的系統調用在內核中的處理絕大部分是一樣的。另外,請注意ecx寄存器,mov ecx,0x7ffe0304將其值設為0x7ffe0304,也就是SharedUserData內存區里SystemCallStub例程中ret指令的地址(參見上文的SystemCallStub代碼)。在進入nt!KiSystemService之前,ecx連同其他一些參數被壓入棧中。事實上,ecx用來指定SYSEXIT返回用戶模式時的目標地址。當使用INT 2E進行系統調用時,由于INT n指令會自動將中斷發生時的CS和EIP寄存器壓入棧中,當中斷處理例程通過執行iretd返回時,iretd指令會使用棧中保存的CS和EIP值返回合適的位置。因為sysenter指令不會向棧中壓入要返回的位置,所以sysexit指令必須通過其他機制知道要返回的位置。這便是壓入ECX寄存器的原因。通過反匯編KiSystemCallExit2例程,我們可以看到在執行sysexit指令之前,ecx寄存器的值又從棧中恢復出來了。

    kd> u nt!KiSystemCallExit l20
    nt!KiSystemCallExit:
    804db3b4 cf            iretd
    nt!KiSystemCallExit2:
    804db3b5 5a            pop      edx
    804db3b6 83c408        add      esp,0x8
    804db3b9 59            pop      ecx
    804db3ba fb            sti
    804db3bb 0f35          sysexit
    nt!KiSystemCallExit3:
    804db3bd 59            pop      ecx
    804db3be 83c408        add      esp,0x8
    804db3c1 5c            pop      esp
    804db3c2 0f07          sysret

    以上代碼中包含了3個從系統調用返回的例程,即KiSystemCallExitKiSystemCallExit2KiSystemCallExit3,它們分別對應于使用INT 2E、sysenter和syscall發起的系統調用,如表2-2所示。

    表2-2 系統調用(略)

    圖2-5展示了使用sysenter/sysexit指令對進行系統調用的完整過程(以調用ReadFile服務為例)。

    圖2-5 快速系統調用(針對IA-32處理器)


    格物

    下面通過一個小的實驗來加深大家對系統調用的理解。首先啟動WinDBG程序,選擇File → Open Crash Dump,然后選擇本書實驗文件中的dumps\w732cf4.dmp文件。在調試會話建立后,先執行.symfix c:\symbols和.reload加載模塊與符號,再執行k命令,便得到清單2-4所示的完美棧回溯。

    第22章將詳細講解棧回溯的原理,現在大家只要知道棧上記錄著函數相互調用時的參數和返回地址等信息。棧回溯是從棧上找到這些信息,然后顯示出來的過程,是追溯線程執行軌跡的一種便捷方法。

    清單2-4還顯示了任務管理器程序(taskmgr)調用NtTerminateProcess系統服務時的執行過程。棧回溯的結果包含4列,第一列是序號,第二列是每個函數的棧幀基地址,第三列是返回地址,第四列是使用“函數名+字節偏移量”形式表達的執行位置。以00棧幀為例,它對應的函數是著名的藍屏函數KeBugCheckEx,它的棧幀基地址是9796fb9c,它的返回地址是82b1ab51,翻譯成符號便是PspCatchCriticalBreak+0x71。

    清單2-4 完美棧回溯

    # ChildEBP RetAddr  
    00 9796fb9c 82b1ab51 nt!KeBugCheckEx+0x1e
    01 9796fbc0 82a6daa8 nt!PspCatchCriticalBreak+0x71
    02 9796fbf0 82a605b6 nt!PspTerminateAllThreads+0x2d
    03 9796fc24 8287c87a nt!NtTerminateProcess+0x1a2
    04 9796fc24 77da7094 nt!KiFastCallEntry+0x12a
    05 001df4dc 77da68d4 ntdll!KiFastSystemCallRet
    06 001df4e0 76193c82 ntdll!NtTerminateProcess+0xc
    07 001df4f0 00bf57b9 KERNELBASE!TerminateProcess+0x2c
    08 001df524 00bf67ec taskmgr!CProcPage::KillProcess+0x116
    09 001df564 00bebc96 taskmgr!CProcPage::HandleWMCOMMAND+0x10f
    0a 001df5d8 76abc4e7 taskmgr!ProcPageProc+0x275
    0b 001df604 76ad5b7c USER32!InternalCallWinProc+0x23
    0c 001df680 76ad59f3 USER32!UserCallDlgProcCheckWow+0x132
    0d 001df6c8 76ad5be3 USER32!DefDlgProcWorker+0xa8
    0e 001df6e4 76abc4e7 USER32!DefDlgProcW+0x22
    0f 001df710 76abc5e7 USER32!InternalCallWinProc+0x23
    10 001df788 76ab5294 USER32!UserCallWinProcCheckWow+0x14b
    11 001df7c8 76ab5582 USER32!SendMessageWorker+0x4d0
    12 001df7e8 74e94601 USER32!SendMessageW+0x7c
    13 001df808 74e94663 COMCTL32!Button_NotifyParent+0x3d
    14 001df824 74e944ed COMCTL32!Button_ReleaseCapture+0x113
    15 001df884 76abc4e7 COMCTL32!Button_WndProc+0xa18
    16 001df8b0 76abc5e7 USER32!InternalCallWinProc+0x23
    17 001df928 76abcc19 USER32!UserCallWinProcCheckWow+0x14b
    18 001df988 76abcc70 USER32!DispatchMessageWorker+0x35e
    19 001df998 76ab41eb USER32!DispatchMessageW+0xf
    1a 001df9bc 00be16fc USER32!IsDialogMessageW+0x588
    1b 001dfdac 00be5384 taskmgr!wWinMain+0x5d1
    1c 001dfe40 76bbed6c taskmgr!_initterm_e+0x1b1
    1d 001dfe4c 77dc377b kernel32!BaseThreadInitThunk+0xe
    1e 001dfe8c 77dc374e ntdll!__RtlUserThreadStart+0x70
    1f 001dfea4 00000000 ntdll!_RtlUserThreadStart+0x1b

    仔細觀察清單2-4中的地址部分,很容易看出用戶空間和內核空間的分界,也就是在棧幀04和棧幀05之間。棧幀05中的KiFastSystemCallRet函數屬于ntdll模塊,位于用戶空間。棧幀04中的KiFastCallEntry函數屬于nt模塊,位于內核空間。棧幀04的基地址是9796fc24,屬于內核空間;棧幀05的基地址是001df4dc,屬于用戶空間。它們分別來自這個線程的內核態棧和用戶態棧。WinDBG的k命令穿越兩個空間,遍歷兩個棧,顯示出線程在用戶空間和內核空間執行的完整過程,能產生如此完美的棧回溯顯示了WinDBG的強大。


    2.6.4 逆向調用

    前文介紹了從用戶模式進入內核模式的兩種方法,通過這兩種方法,用戶模式的代碼可以“調用”位于內核模式的系統服務。那么內核模式的代碼是否可以主動調用用戶模式的代碼呢?答案是肯定的,這種調用通常稱為逆向調用(reverse call)。

    簡單來說,逆向調用的過程是這樣的。首先內核代碼使用內核函數KiCallUserMode發起調用。接下來的執行過程與從系統調用返回(KiServiceExit)類似,不過進入用戶模式時執行的是NTDll.DLL中的KiUserCallbackDispatcher。而后KiUserCallbackDispatcher會調用內核希望調用的用戶態函數。當用戶模式的工作完成后,執行返回動作的函數會執行INT 2B指令,也就是觸發一個0x2B異常。這個異常的處理函數是內核模式的KiCallbackReturn函數。于是,通過INT 2B異常,CPU又跳回內核模式繼續執行了。

    lkd> !idt 2b
    Dumping IDT:
    2b:   8053d070 nt!KiCallbackReturn

    以上是使用WinDBG的!idt命令觀察到的0x2B異常的處理函數。

    2.6.5 實例分析

    下面通過一個實際例子來進一步展示系統調用和逆向調用的執行過程。清單2-5顯示了使用WinDBG的內核調試會話捕捉到的記事本進程發起系統調用進入內核和內核函數執行逆向調用的全過程(棧回溯)。

    清單2-5 記事本進程從發起系統調用進入內核和內核函數逆向調用的全過程

    kd> kn
     # ChildEBP RetAddr  
    00 0006fe94 77fb4da6 USER32!XyCallbackReturn
    01 0006fe94 8050f8ae ntdll!KiUserCallbackDispatcher+0x13
    02 f4fc19b4 80595d2c nt!KiCallUserMode+0x4
    03 f4fc1a10 bf871e98 nt!KeUserModeCallback+0x87
    04 f4fc1a90 bf8748d4 win32k!SfnDWORD+0xa0
    05 f4fc1ad8 bf87148d win32k!xxxSendMessageToClient+0x174
    06 f4fc1b24 bf8714d3 win32k!xxxSendMessageTimeout+0x1a6
    07 f4fc1b44 bf8635f6 win32k!xxxSendMessage+0x1a
    08 f4fc1b74 bf84a620 win32k!xxxMouseActivate+0x22d
    09 f4fc1c98 bf87a0c1 win32k!xxxScanSysQueue+0x828
    0a f4fc1cec bf87a8ad win32k!xxxRealInternalGetMessage+0x32c
    0b f4fc1d4c 804da140 win32k!NtUserGetMessage+0x27
    0c f4fc1d4c 7ffe0304 nt!KiSystemService+0xc4
    0d 0006feb8 77d43a21 SharedUserData!SystemCallStub+0x2
    0e 0006febc 77d43c95 USER32!NtUserGetMessage+0xc
    0f 0006fed8 010028e4 USER32!GetMessageW+0x31
    10 0006ff1c 01006c54 notepad!WinMain+0xe3
    11 0006ffc0 77e814c7 notepad!WinMainCRTStartup+0x174
    12 0006fff0 00000000 kernel32!BaseProcessStart+0x23

    根據執行的先后順序,最下面一行(幀#12)對應的是進程的啟動函數BaseProcessStart,而后是編譯器生成的進程啟動函數WinMainCRTStartup,以及記事本程序自己的入口函數WinMain。幀#0f表示記事本程序在調用GetMessage API進入消息循環。接下來GetMessage API調用Windows子系統服務的殘根函數NtUserGetMessage。從第2列的棧幀基地址都小于0x800000000可以看出,幀#12~#0d都是在用戶模式執行的。幀#0d執行我們前面分析過的SystemCallStub,而后(幀#0c)便進入了內核模式的KiSystemServiceKiSystemService根據系統服務號碼,將調用分發給Windows子系統內核模塊win32k中的NtUserGetMessage函數。

    幀#0a~#05表示內核模式的窗口消息函數在工作。幀#07~#05表示要把一個窗口消息發送到用戶態。幀#04的SfnDWORD表示在將消息組織好后調用KeUserModeCallback函數,發起逆向調用。幀#02表明在執行KiCallUserMode函數,幀#01表明已經在用戶模式下執行,這兩行之間的部分過程沒有顯示出來。同樣,幀#01 和幀#00 之間執行用戶模式函數的過程沒有完全體現出來。XyCallbackReturn函數是用于返回內核模式的,它的代碼很簡單,只有如下幾條指令。

    USER32!XyCallbackReturn:
    001b:77d44168 8b442404     mov   eax,dword ptr [esp+4] ss:0023:0006fe84=00000000
    001b:77d4416c cd2b          int   2Bh
    001b:77d4416e c20400        ret   4

    第1行把用戶模式函數的執行結果賦給EAX寄存器,第2行執行INT 2B指令。執行過INT 2B后,CPU便轉去執行異常處理程序KiCallbackReturn,回到了內核模式。

    本文摘自《軟件調試(第2版)卷2:Windows平臺調試(上、下冊)》

    本書是國內當前集中介紹軟件調試主題的權威著作。本書第2卷分為5篇,共30章,主要圍繞Windows系統展開介紹。第一篇(第1~4章)介紹Windows系統簡史、進程和線程、架構和系統部件,以及Windows系統的啟動過程,既從空間角度講述Windows的軟件世界,也從時間角度描述Windows世界的搭建過程。第二篇(第5~8章)描述特殊的過程調用、墊片、托管世界和Linux子系統。第三篇(第9~19章)深入探討用戶態調試模型、用戶態調試過程、中斷和異常管理、未處理異常和JIT調試、硬錯誤和藍屏、錯誤報告、日志、事件追蹤、WHEA、內核調試引擎和驗證機制。第四篇(第20~25章)從編譯和編譯期檢查、運行時庫和運行期檢查、棧和函數調用、堆和堆檢查、異常處理代碼的編譯、調試符號等方面概括編譯器的調試支持。第五篇(第26~30章)首先縱覽調試器的發展歷史、工作模型和經典架構,然后分別討論集成在Visual Studio和Visual Studio(VS)Code中的調試器,最后深度解析WinDBG調試器的歷史、結構和用法。

    本書理論與實踐結合,不僅涵蓋了相關的技術背景知識,還深入研討了大量具有代表性的技術細節,是學習軟件調試技術的珍貴資料。

    主要內容

    針對進程行為的監控需求,以往很多安全軟件都是采用的Hook技術攔截關鍵的系統調用,來實現對惡意軟件進程創建的攔截。但在x64架構下,系統內核做了很多安全檢測措施,特別是類似于KDP這樣的技術,使得Hook方法不再有效。為此OS推出了基于回調實現的行為監控方案。本文借助IDA逆向分析該技術的實現原理并給出了關鍵數據結構及調用鏈,通過雙機內核調試驗證了該數據結構以及調用鏈的正確性。

    涉及到的內容如下:

    1、內核對象及內核對象管理;
    2、進程回調;
    3、內核調試;
    4、Windbg雙擊調試;

    0 引言

    近年來,各種惡意軟件新變種層出不窮,攻擊方法、手段多種多樣,造成了巨大的經濟損失。作為防守的第一個環節就是能夠識別出惡意進程創建的動作,而進程創建監控技術是為了能夠讓安全軟件有機會攔截到此動作的技術。安全軟件根據匹配算法判斷是否準許該進程創建,以此達到保護用戶數據安全的目的。x86架構下的實現方案多為Hook技術,通過攔截內核中進程創建的關鍵API如nt!NtCreateProcess或nt!NtCreateProcessEx,通過堆棧來回溯到關鍵參數,如待創建進程的exe全路徑、父進程信息,然后根據獲取到的全路徑檢測exe磁盤文件,同時也可以分析進程鏈最終確定是否放行該動作。但這種技術方案存在一些缺陷,一方面其破壞了內核的完整性,導致系統的穩定性下降;另一方面,這些API很多都是未公開的,也就意味著需要通過逆向工程等技術手段來分析OS內核鏡像文件,定位到關鍵的API。但如果系統升級了,該API可能就不存在了,這也導致安全軟件的兼容性特別差;最重要的是各個安全廠家的實現方案不一樣,掛鉤的點也不同,很容易出現相互競爭的情況,極有可能會導致BSoD(Blue Screen of Death)。另一種傳統的基于特征碼的攔截方式,也同樣存在類似的問題。需要為每個子版本的系統關鍵API做逆向分析,取出特征碼,當系統更新或者打補丁,則需要再次逆向分析取出特征碼。工作量巨大,效率低下,適配性很低,如果沒有及時更新特征碼,很可能會使得監控失效,情況糟糕的時候會直接導致BSoD。為此,在x64架構下,內核一方面為了保護關鍵數據的完整性,另一方面也為了提高內核程序自身的穩定性,推出了諸如KDP(Kernel Data Protection)、PG等安全措施,使得傳統的 Hook技術失效;同時OS為了規范化安全相關信息的獲取,使得安全軟件能夠在內核可控的情況下提供安全服務,Windows系統層面提供了一種基于回調的方式來通知安全軟件注冊的內核回調例程。這種方式優點是方便高效,可移植性好,穩定性高,且各個安全廠商之間也不會出現競爭的關系。

    本文基于逆向工程及內核調試技術,分析了該技術的具體實現及系統額外增加的數據檢測機制。借助逆向工具IDA靜態逆向分析了系統關鍵API的內部動作及具體的實現,相關的數據結構,得到該技術實際觸發的調用源以及整個調用鏈。借助VMWare搭建雙機調試環境,利用Windbg動態調試系統內核,查看系統中所涉及到的關鍵數據,并與PCHunter給出的數據做對比分析,驗證了分析結論的正確性。此外還通過對調用鏈中的關鍵函數下斷點,通過棧回溯技術,動態觀察了整個調用鏈及觸發時間。分析得到的關鍵數據結構和系統對數據做的檢測校驗算法可用于檢測病毒木馬等軟件惡意構造的表項,且還可以應用到安全廠商對抗惡意代碼時,自動構造表項來檢測系統行為,完全脫離系統提供的注冊卸載API。

    1 進程回調原理分析

    1.1 安裝與卸載逆向分析

    根據微軟官方技術文檔MSDN上的說明,通過PsSetCreateProcessNotifyRoutine、PsSetCreateProcessNotifyRoutineEx和PsSetCreateProcessNotifyRoutineEx2這三API來安裝一個進程創建、退出通知回調例程,當有進程創建或者退出時,系統會回調參數中指定的函數。以PsSetCreateProcessNotifyRoutine為例子,基于IDA逆向分析該API的具體實現。如圖1所示,由圖可知,該API內部僅僅是簡單的調用另一個函數,其自身僅僅是一個stub,具體的實現在PspSetCreateProcessNotifyRoutine中,此函數的安裝回調例程的關鍵實現如圖所示。

    調用ExAllocateCallBack,創建出了一個回調對象,并將pNotifyRoutine和bRemovel作為參數傳入,以初始化該回調對象,代碼如圖所示;其中pNotifyRoutine即是需要被回調的函數例程,此處的bRemovel為false,表示當前是安裝回調例程。

    緊接著調用ExCompareExchangeCallBack將初始化好的CallBack對象添加到PspCreateProcessNotifyRoutine所維護的全局數組中。值得注意的是,ExCompareExchangeCallBack中在安裝回調例程時,對回調例程有一個特殊的操作如圖所示。

    與0x0F做了或操作,等價于將低4位全部置1;若ExCompareExchangeCallBack執行失敗,則接著下一輪循環繼續執行。由圖2中第66行代碼可知,循環的最大次數是0x40次。如果一直失敗,可調用ExFreePoolWithTag釋放掉pCallBack所占用的內存,且返回0xC000000D錯誤碼。

    然后根據v3的值判斷是通過上述三個API中的哪個安裝的回調,來更新相應的全局變量。其中PspCreateProcessNotifyRoutineExCount和PspCreateProcessNotifyRoutineCount分別記錄當前通過PsSetCreateProcessNotifyRoutineEx和PsSetCreateProcessNotifyRoutine安裝回調例程的個數。PspNotifyEnableMask用以表征當前數組中是否安裝了回調例程,該值在系統遍歷回調數組執行回調例程時,用以判斷數組是否為空,加快程序的執行效率。

    除了能夠安裝回調例程,這三個API也能卸載指定的回調例程。以PsSetCreateProcessNotifyRoutine為例,分析其實現的關鍵部分,如圖所示。

    通過一個while循環遍歷PspCreateProcessNotifyRoutine數組,調用ExReferenceCallBackBlock取出數組中的每一項,該API內部會做一些檢驗動作且對返回的數據也做了特殊處理,如圖所示。圖6中*pCallBackObj即是取出回調對象中的回調例程的函數地址,通過判斷其低4位是否為1來做一些數據的校驗,如17行所示。系統做這個處理也是起到保護作用,防止惡意構造數據填入表中,劫持正常的系統調用流程。此外,圖中第33行處的代碼,在將回調例程返回給父調用時,也將回調例程的低4位全部清零,否則返回的地址是錯誤的,調用立馬觸發CPU異常。

    ExReferenceCallBackBlock成功返回后,調用ExGetCallBackBlockRoutine從返回的回調對象中取出回調例程,并判斷取出的是否為當前指定需要卸載的項,如果是則調用ExDereferenceCallBackBlock遞減引用計數,接著調用ExFreePoolWithTag釋放掉Callback所占用的內存。期間也會更新PspCreateProcessNotifyRoutineExCount或PspCreateProcessNotifyRoutineCount的值。根據源碼還可以得知,該數組總計64項,也即只能安裝64個回調例程。如果遍歷完數組的64項依舊沒有找到,則返回0xC000007A錯誤碼。

    1.2 OS執行回調例程分析

    回調例程安裝完之后,如果有新的進程創建或退出,內核則便會遍歷該數組來執行其中安裝的每一項回調例程。通過IDA的交叉引用功能,可分析出內核其他地方對PspCreateProcessNotifyRoutine的交叉引用,如圖所示,

    共計5個地方涉及到此變量。其中PspCallProcessNotifyRoutines是直接調用回調例程的函數,該函數的關鍵部分如圖所示。

    通過while循環,遍歷PspCreateProcessNotifyRoutine數組中安裝的所有回調例程,依次執行。PspNotifyEnableMask & 2的操作即為判斷當前數組中是否安裝有回調例程,加快程序的執行效率,這個變量的值在PsSetCreateProcessNotifyRoutine中安裝回調例程時設置。bRemove & 2這個if分支,是用來判斷當前的回調例程是通過PsSetCreateProcessNotifyRoutine還是PsSetCreateProcessNotifyRoutineEx安裝,因為這兩個API安裝的回調例程的原型不同,在實際調用時傳入的參數也不同。兩者的回調例程原分別為:void PcreateProcessNotifyRoutine(HANDLE ParentId,HANDLE ProcessId,BOOLEAN Create)和void PcreateProcessNotifyRoutineEx(PEPROCESS Process,HANDLE ProcessId,PPS_CREATE_NOTIFY_INFO CreateInfo)。此外,圖8中IDA給出的偽C代碼RoutineFun((unsigned __int64)RoutineFun)明顯不對,因為回調例程的參數個數是3個,而IDA分析出的參數只有1個,顯然有問題。直接看下反匯編代碼即可得知,如圖所示,

    根據x64下的調用約定可知,函數的前4個參數是通過rcx、rdx、r8和r9這四個寄存器傳遞,圖給出的正是回調例程的前三個參數,_guard_dispatch_icall內部會直接取rax的值調用過去,而rax的值正是ExGetCallBackBlockRoutine調用返回的回調例程函數地址。

    上圖中的第二個涉及到PspCreateProcessNotifyRoutine數組的是PspEnumerateCallback函數,該函數是系統內部函數,沒有導出,其具體實現如圖所示。

    該函數根據dwEnumType來判斷想要枚舉的是哪個數組,由代碼分析可知,系統內核維護了三個回調相關的數組,分別為鏡像加載回調數組,進程創建退出回調數組,線程創建退出數組。類似之前的函數校驗,這里也檢測了索引是否超過0x40,超過了則返回0,以示失敗。

    1.3 觸發調用的調用鏈分析

    上節分析了回調例程的直接調用上級函數,本節分析整個調用鏈,主要分析調用源及調用過程中涉及到的關鍵函數。根據IDA給出的交叉引用圖如圖所示。

    涉及到的函數調用非常多,很多不相關的也被包含進來,不便于分析。經手動分析整理后的調用鏈,其鏈路中的關鍵API如圖所示。

    虛線以上部分為用戶態程序,虛線以下為內核態程序,紅色標注的都是標準導出的API。根據圖12可知,當用戶態進程調用RtlCreateUserProcess、RtlCreateUserProcesersEx或RtlExitUserProcess時,內核都會去遍歷PspCreateProcessNotifyRoutine數組,依次執行回調例程,通知給驅動程序做相應的處理。驅動接管之后,可以做安全校驗處理,分析進程的父進程或者進一步分析進程鏈,此外還可以對即將被拉起的子進程做特征碼匹配,PE指紋識別,導入表檢測等防御手段。這種方式不需要去Hook任何API,也無需做特征碼定位等重復繁瑣的工作,完全基于系統提供的回調機制,且在Windows系統中都可以無縫銜接。且各個安全廠家之間也不存在相互競爭,大大縮小了系統藍屏的風險。圖12中NtCreateUserProcess調用PspInsertThread的原因是創建進程的API內部會創建該進程的主線程。將遍歷回調例程數組的工作統一到PspInsertThread中,由其去調用下層的PspCallProcessNotifyRoutines更為合理。

    2 實驗

    2.1 觀察系統中已安裝的回調例程

    實驗環境如表1所示,借助于VMWare進行雙機調試。

    Guest OS Build 10.0.16299.125
    Host OS Build 10.0.17134.885
    Windbg版本 10.0.17134.1
    VMWare 14.1.1 build-7528167
    PCHunter V1.56

    在Windbg中觀察PspCreateProcessNotifyRoutine數組,共計14項有效數據,如下所示;

    1: kd> dd PspCreateProcessNotifyRoutineCount  l1
    fffff802`151f4e78  00000009
    
    1: kd> dd PspCreateProcessNotifyRoutineExCount l1
    fffff802`151f4e7c  00000005
    
    1: kd> dq PspCreateProcessNotifyRoutine l40
    fffff802`14da2a80  ffffcc8b`d884b9bf  ffffcc8b`d8d9c96f
    fffff802`14da2a90  ffffcc8b`d939975f  ffffcc8b`da00044f
    fffff802`14da2aa0  ffffcc8b`d9bd382f  ffffcc8b`da41e8df
    fffff802`14da2ab0  ffffcc8b`da53815f  ffffcc8b`da5ca8bf
    fffff802`14da2ac0  ffffcc8b`dac5178f  ffffcc8b`dbef624f
    fffff802`14da2ad0  ffffcc8b`dce333af  ffffcc8b`dcec67df
    fffff802`14da2ae0  ffffcc8b`dc735def  ffffcc8b`dcabd32f
    
    拆解第一項,尋找其所對應的回調例程,如下:
    1: kd> dq ffffcc8b`d884b9b0 l3
    ffffcc8b`d884b9b0  00000000`00000020 fffff802`13fd6268
    ffffcc8b`d884b9c0  00000000`00000000
    由此可知,安裝的回調例程起始地址為fffff802`13fd6268,且還可知道Remove為0,即這個是已經安裝的。尋找該回調例程對應的驅動模塊,如下:
    
    1: kd> u fffff802`13fd6268
    360qpesv64+0x26268:
    fffff802`13fd6268  mov  qword ptr [rsp+08h],rbx
    fffff802`13fd626d  mov  qword ptr [rsp+10h],rbp
    fffff802`13fd6272  mov  qword ptr [rsp+18h],rsi
    fffff802`13fd6277  push rdi
    
    1: kd> lmvm 360qpesv64
    start              end                 module name
    fffff802`13fb0000 fffff802`14002000 360qpesv64
    Loaded symbol image file: 360qpesv64.sys
    Image path: 360qpesv64.sys
    Image name: 360qpesv64.sys
    Timestamp:  Wed May 27 20:13:22 2020 (5ECF2C52)
    CheckSum:   00054A2A
    ImageSize:  00052000
    

    可知該回調例程是360官方提供。借助PCHunter來對比下,其給出的數據如圖所示,

    2.2 動態調試回調例程

    以表項的第14項為例,內容如下,

    1: kd> dq ffffcc8b`dcabd320 l3
    ffffcc8b`dcabd320  00000000`00000020 fffff802`13d795b4
    ffffcc8b`dcabd330  00000000`00000006
    
    1: kd> bp fffff802`13d795b4
    1: kd> g
    
    斷點命中,查看父進程相關信息,如下,
    Breakpoint 0 hit
    fffff802`13d795b4 48895c2408      mov     qword ptr [rsp+8],rbx
    1: kd> dt _EPROCESS @$proc -yn ImageFileName
    nt!_EPROCESS
      +0x450 ImageFileName : [15]  "svchost.exe"
    由此可知,是svchost.exe這個父進程創建或者銷毀了一個子進程,更具體的信息如下分析;查看下當前的上下文環境;
    
    1: kd> r
    rax=fffff80213d795b4 rbx=ffffcb8050526c80 rcx=ffffcc8bdd67e080
    rdx=0000000000001f28 rsi=000000000000000d rdi=ffffcc8bdd67e080
    rip=fffff80213d795b4 rsp=ffffcb8050526c38 rbp=ffffcb8050526ca9
    r8=ffffcb8050526c80  r9=ffffcc8bdc735de0 r10=ffff9401cdcc2760
    r11=0000000000000000 r12=0000000000000001 r13=0000000000000000
    r14=ffffcc8bdcabd320 r15=fffff80214da2ae8
    iopl=0         nv up ei pl zr na po nc
    cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
    
    根據x64的調用約定可知,rcx寄存器中存儲的是EPROCESS對象指針,該對象存儲的是即將被創建的子進程的相關信息,可以獲取到的作為身份識別或者安全檢測的關鍵信息如下:
    1: kd> dt _EPROCESS ffffcc8bdd67e080 -yn ImageFile
    ntdll!_EPROCESS
       +0x448 ImageFilePointer : 0xffffcc8b`dc97c5c0 _FILE_OBJECT
       +0x450 ImageFileName : [15]  "UpdateAssistan"
    
    1: kd> dt 0xffffcc8b`dc97c5c0 _FILE_OBJECT -yn FileName
    ntdll!_FILE_OBJECT
       +0x058 FileName : _UNICODE_STRING "\Windows\UpdateAssistant\UpdateAssistant.exe"
    
    1: kd> .process /p ffffcc8bdd67e080; !peb 186ef07000
    Implicit process is now ffffcc8b`dd67e080
    .cache forcedecodeuser done
    PEB at 000000186ef07000
        CurrentDirectory:  'C:\Windows\system32\'
        WindowTitle:  'C:\Windows\UpdateAssistant\UpdateAssistant.exe'
        ImageFile:    'C:\Windows\UpdateAssistant\UpdateAssistant.exe'
    CommandLine:  'C:\Windows\UpdateAssistant\UpdateAssistant.exe /ClientID Win10Upgrade:VNL:NHV19:{} /CalendarRun'
    
    可以獲取到該進程的EXE路徑,創建時的命令行參數,父進程的PID等信息,這些足以用于安全軟件的檢測。
    父進程的完整調用棧如下,
    
    1: kd> k
     # Child-SP          RetAddr           Call Site
    00 ffffcb80`50526c38 fffff802`14ef4ae5 0xfffff802`13d795b4
    01 ffffcb80`50526c40 fffff802`14ef752c nt!PspCallProcessNotifyRoutines+0x249
    02 ffffcb80`50526d10 fffff802`14f2797b nt!PspInsertThread+0x5a4
    03 ffffcb80`50526dd0 fffff802`14b79553 nt!NtCreateUserProcess+0x9c7
    04 ffffcb80`50527a10 00007ffe`547d1654 nt!KiSystemServiceCopyEnd+0x13
    05 0000002f`4b67d258 00007ffe`50b406df ntdll!NtCreateUserProcess+0x14
    06 0000002f`4b67d260 00007ffe`50b3d013 KERNELBASE!CreateProcessInternalW+0x1b3f
    07 0000002f`4b67dec0 00007ffe`5216ee0f KERNELBASE!CreateProcessAsUserW+0x63
    08 0000002f`4b67df30 00007ffe`4ce0a136 KERNEL32!CreateProcessAsUserWStub+0x5f
    09 0000002f`4b67dfa0 00007ffe`4ce0bdd9 UBPM!UbpmpLaunchAction+0xb36
    0a 0000002f`4b67e280 00007ffe`4ce08ee0 UBPM!UbpmLaunchTaskExe+0x279
    0b 0000002f`4b67e490 00007ffe`4ce10a86 UBPM!UbpmpLaunchOneTask+0x6c0
    0c 0000002f`4b67e8f0 00007ffe`4ce0b8bc UBPM!UbpmpHandleGroupSid+0x236
    0d 0000002f`4b67ea10 00007ffe`4ce0b78b UBPM!UbpmpLaunchExeAction+0xec
    0e 0000002f`4b67eaf0 00007ffe`4ce0b5a3 UBPM!UbpmpTakeAction+0xeb
    0f 0000002f`4b67eb50 00007ffe`4ce0b193 UBPM!UbpmpPerformTriggerActions+0x293
    10 0000002f`4b67eca0 00007ffe`4ce1316c UBPM!UbpmpHandleTriggerArrived+0x563
    11 0000002f`4b67ef50 00007ffe`508c32d0 UBPM!UbpmpRepetitionArrived+0x1c
    12 0000002f`4b67ef90 00007ffe`508c3033 EventAggregation!EaiSignalAggregateEvent+0x16c
    13 0000002f`4b67f060 00007ffe`508c27aa EventAggregation!EaiSignalCallback+0xe7
    14 0000002f`4b67f140 00007ffe`508c253e EventAggregation!EaiProcessNotification+0x1aa
    15 0000002f`4b67f270 00007ffe`508caef8 EventAggregation!WnfEventCallback+0x506
    16 0000002f`4b67f3a0 00007ffe`5476769f EventAggregation!AggregateEventWnfCallback+0x38
    17 0000002f`4b67f3f0 00007ffe`54767a51 ntdll!RtlpWnfWalkUserSubscriptionList+0x29b
    18 0000002f`4b67f4e0 00007ffe`5476b510 ntdll!RtlpWnfProcessCurrentDescriptor+0x105
    19 0000002f`4b67f560 00007ffe`54766b59 ntdll!RtlpWnfNotificationThread+0x80
    1a 0000002f`4b67f5c0 00007ffe`54764b70 ntdll!TppExecuteWaitCallback+0xe1
    1b 0000002f`4b67f600 00007ffe`52171fe4 ntdll!TppWorkerThread+0x8d0
    1c 0000002f`4b67f990 00007ffe`5479ef91 KERNEL32!BaseThreadInitThunk+0x14
    1d 0000002f`4b67f9c0 00000000`00000000 ntdll!RtlUserThreadStart+0x21
    
    由于前四個參數是通過的寄存器傳遞的,所以無法直接通過棧來回溯到參數,但可以通過手動方式分析得到。分析ntdll!NtCreateUserProcess的調用父函數,其返回地址處的匯編代碼如下所示:
    
    1: kd> ub 00007ffe`50b406df
    KERNELBASE!CreateProcessInternalW+0x1b11:
    00007ffe`50b406b1 488b842440040000 mov     rax,qword ptr [rsp+440h]
    00007ffe`50b406b9 4889442420      mov     qword ptr [rsp+20h],rax
    00007ffe`50b406be b800000002      mov     eax,2000000h
    00007ffe`50b406c3 448bc8          mov     r9d,eax
    00007ffe`50b406c6 448bc0          mov     r8d,eax
    00007ffe`50b406c9 488d942448010000 lea     rdx,[rsp+148h]
    00007ffe`50b406d1 488d8c24e0000000 lea     rcx,[rsp+0E0h]
    00007ffe`50b406d9 ff1521901600    call    qword ptr [KERNELBASE!_imp_NtCreateUserProcess (00007ffe`50ca9700)]
    
    可知,NtCreateUserProcess第一個參數和第二個參數再rsp+0xE0和rsp+0x148處;查看該處的數據如下:
    1: kd> dpu 0000002f`4b67d260+E0 0000002f`4b67d260+148 
    0000002f`4b67d340  00000000`00000000
    0000002f`4b67d348  00000000`00000004
    0000002f`4b67d350  00000100`00000000
    0000002f`4b67d358  00000000`00000020
    0000002f`4b67d360  000001f2`d9b87cc0 "C:\Windows\UpdateAssistant\UpdateAssistant.exe"
    0000002f`4b67d368  00000000`00000000
    0000002f`4b67d370  00000000`00000000
    0000002f`4b67d378  0000002f`00000000
    0000002f`4b67d380  000001f2`d8d43580 "C:\Windows\UpdateAssistant\UpdateAssistant.exe /ClientI"
    0000002f`4b67d388  00000000`00000000
    0000002f`4b67d390  00000000`00008664
    0000002f`4b67d398  000001f2`d9d73c40 "ALLUSERSPROFILE=C:\ProgramData"
    0000002f`4b67d3a0  00000000`00000000
    0000002f`4b67d3a8  00000000`00000000
    

    由此可知,svchost拉起的子進程為UpdateAssistant.exe,與之前分析得到的參數也相吻合。從調用棧可知,是在svchost創建子進程UpdateAssistant.exe時遍歷的回調例程,通知給驅動軟件做相應的處理。

    3 結束語

    本文詳細地分析了系統實現進程回調安全機制的內部原理,借助IDA工具逆向系統鏡像文件,分析了實現的關鍵代碼部分,得到了關鍵數據結構及系統額外做的數據檢測校驗算法。對關鍵全局變量的作用也做了詳細解釋。此外,通過逆向分析,給出了整個機制的調用源與調用鏈。最后基于雙機調試環境,動態查看內核中維護的進程回調例程表,并且下斷點實際動態調試了整個過程。對于驅動開發,內核安全相關方面的研究工作者提供了該技術實現原理與機制。基于得到的關鍵數據結構和系統數據檢驗保護算法,可以解密關鍵字段后檢測表項中的惡意代碼,也可以用于安全廠商在對抗過程中,完全脫離系統提供的API手工構建表項,達到監控系統行為的目的。

網站首頁   |    關于我們   |    公司新聞   |    產品方案   |    用戶案例   |    售后服務   |    合作伙伴   |    人才招聘   |   

友情鏈接: 餐飲加盟

地址:北京市海淀區    電話:010-     郵箱:@126.com

備案號:冀ICP備2024067069號-3 北京科技有限公司版權所有