主頁 > 後端開發 > 驅動開發:內核列舉DpcTimer定時器

驅動開發:內核列舉DpcTimer定時器

2022-10-16 11:45:27 後端開發

在筆者上一篇文章《驅動開發:內核列舉IoTimer定時器》中我們通過IoInitializeTimer這個API函式為跳板,向下掃描特征碼獲取到了IopTimerQueueHead也就是IO定時器的佇列頭,本章學習的列舉DPC定時器依然使用特征碼掃描,唯一不同的是在新版系統中DPC是被異或加密的,想要找到正確的地址,只是需要在找到DPC表頭時進行解密操作即可,

DPC定時器的作用: 在內核中可以使用DPC定時器設定任意定時任務,當到達某個節點時自動觸發定時回呼,定時器的內部使用KTIMER物件,當設定任務時會自動插入到DPC佇列,由作業系統回圈讀取DPC佇列并執行任務,列舉DPC定時器可得知系統中存在的DPC任務,

要想在新版系統中得到DPC定時器則需要執行以下步驟

  • 1.找到KiProcessorBlock地址并決議成_KPRCB結構
  • 2.在_KPRCB結構中得到_KTIMER_TABLE偏移
  • 3.決議_KTIMER_TABLE_ENTRY得到加密后的雙向鏈表

首先_KPRCB這個結構體與CPU內核對應,獲取方式可通過一個未匯出的變數nt!KiProcessorBlock來得到,如下雙核電腦,結構體存在兩個與之對應的結構地址,

lyshark.com 0: kd> dq nt!KiProcessorBlock
fffff807`70a32cc0  fffff807`6f77c180 ffffbe81`3cee0180
fffff807`70a32cd0  00000000`00000000 00000000`00000000
fffff807`70a32ce0  00000000`00000000 00000000`00000000

KiProcessorBlock是一個陣列,其第一個結構體TimerTable則是結構體的偏移,

lyshark.com 0: kd> dt _KPRCB fffff807`6f77c180
ntdll!_KPRCB
   +0x000 MxCsr            : 0x1f80
   +0x3680 TimerTable       : _KTIMER_TABLE (此處)
   +0x5880 DpcGate          : _KGATE

接下來是把所有的KTIMER都列舉出來,KTIMER在TimerTable中的存盤方式是陣列+雙向鏈表,

lyshark.com 0: kd> dt _KTIMER_TABLE
ntdll!_KTIMER_TABLE
   +0x000 TimerExpiry      : [64] Ptr64 _KTIMER
   +0x200 TimerEntries     : [256] _KTIMER_TABLE_ENTRY (此處)

到了_KTIMER_TABLE_ENTRY這里,Entry開始的雙向鏈表,每一個元素都對應一個Timer也就是說我們已經可以遍歷所有未解密的Time變數了,

lyshark.com 0: kd> dt _KTIMER_TABLE_ENTRY 0xfffff807`6f77c180 + 0x3680
ntdll!_KTIMER_TABLE_ENTRY
   +0x000 Lock             : 0
   +0x008 Entry            : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
   +0x018 Time             : _ULARGE_INTEGER 0x0

lyshark.com 0: kd> dt _KTIMER_TABLE_ENTRY 0xfffff807`6f77c180 + 0x3680 + 0x200
ntdll!_KTIMER_TABLE_ENTRY
   +0x000 Lock             : 0
   +0x008 Entry            : _LIST_ENTRY [ 0xffffa707`a0d3e1a0 - 0xffffa707`a0d3e1a0 ]
   +0x018 Time             : _ULARGE_INTEGER 0x00000001`a8030353

至于如何解密,我們需要得到加密位置,如下通過KeSetTimer找到KeSetTimerEx從中得到DCP加密流程,

lyshark.com 0: kd> u nt!KeSetTimer
nt!KeSetTimer:
fffff803`0fc63a40 4883ec38        sub     rsp,38h
fffff803`0fc63a44 4c89442420      mov     qword ptr [rsp+20h],r8
fffff803`0fc63a49 4533c9          xor     r9d,r9d
fffff803`0fc63a4c 4533c0          xor     r8d,r8d
fffff803`0fc63a4f e80c000000      call    nt!KiSetTimerEx (fffff803`0fc63a60)
fffff803`0fc63a54 4883c438        add     rsp,38h
fffff803`0fc63a58 c3              ret
fffff803`0fc63a59 cc              int     3

0: kd> u nt!KiSetTimerEx l50
nt!KiSetTimerEx:
fffff803`0fc63a60 48895c2408      mov     qword ptr [rsp+8],rbx
fffff803`0fc63a65 48896c2410      mov     qword ptr [rsp+10h],rbp
fffff803`0fc63a6a 4889742418      mov     qword ptr [rsp+18h],rsi
fffff803`0fc63a6f 57              push    rdi
fffff803`0fc63a70 4154            push    r12
fffff803`0fc63a72 4155            push    r13
fffff803`0fc63a74 4156            push    r14
fffff803`0fc63a76 4157            push    r15
fffff803`0fc63a78 4883ec50        sub     rsp,50h
fffff803`0fc63a7c 488b057d0c5100  mov     rax,qword ptr [nt!KiWaitNever (fffff803`10174700)]
fffff803`0fc63a83 488bf9          mov     rdi,rcx
fffff803`0fc63a86 488b35630e5100  mov     rsi,qword ptr [nt!KiWaitAlways (fffff803`101748f0)]
fffff803`0fc63a8d 410fb6e9        movzx   ebp,r9b
fffff803`0fc63a91 4c8bac24a0000000 mov     r13,qword ptr [rsp+0A0h]
fffff803`0fc63a99 458bf8          mov     r15d,r8d
fffff803`0fc63a9c 4933f5          xor     rsi,r13
fffff803`0fc63a9f 488bda          mov     rbx,rdx
fffff803`0fc63aa2 480fce          bswap   rsi
fffff803`0fc63aa5 4833f1          xor     rsi,rcx
fffff803`0fc63aa8 8bc8            mov     ecx,eax
fffff803`0fc63aaa 48d3ce          ror     rsi,cl
fffff803`0fc63aad 4833f0          xor     rsi,rax
fffff803`0fc63ab0 440f20c1        mov     rcx,cr8
fffff803`0fc63ab4 48898c24a0000000 mov     qword ptr [rsp+0A0h],rcx
fffff803`0fc63abc b802000000      mov     eax,2
fffff803`0fc63ac1 440f22c0        mov     cr8,rax
fffff803`0fc63ac5 8b05dd0a5100    mov     eax,dword ptr [nt!KiIrqlFlags (fffff803`101745a8)]
fffff803`0fc63acb 85c0            test    eax,eax
fffff803`0fc63acd 0f85b72d1a00    jne     nt!KiSetTimerEx+0x1a2e2a (fffff803`0fe0688a)
fffff803`0fc63ad3 654c8b342520000000 mov   r14,qword ptr gs:[20h]
fffff803`0fc63adc 33d2            xor     edx,edx
fffff803`0fc63ade 488bcf          mov     rcx,rdi
fffff803`0fc63ae1 e86aa2fdff      call    nt!KiCancelTimer (fffff803`0fc3dd50)
fffff803`0fc63ae6 440fb6e0        movzx   r12d,al
fffff803`0fc63aea 48897730        mov     qword ptr [rdi+30h],rsi
fffff803`0fc63aee 33c0            xor     eax,eax
fffff803`0fc63af0 44897f3c        mov     dword ptr [rdi+3Ch],r15d
fffff803`0fc63af4 8b0f            mov     ecx,dword ptr [rdi]
fffff803`0fc63af6 4889442430      mov     qword ptr [rsp+30h],rax
fffff803`0fc63afb 894c2430        mov     dword ptr [rsp+30h],ecx
fffff803`0fc63aff 488bcb          mov     rcx,rbx
fffff803`0fc63b02 48c1e920        shr     rcx,20h
fffff803`0fc63b06 4889442438      mov     qword ptr [rsp+38h],rax
fffff803`0fc63b0b 4889442440      mov     qword ptr [rsp+40h],rax
fffff803`0fc63b10 40886c2431      mov     byte ptr [rsp+31h],bpl
fffff803`0fc63b15 85c9            test    ecx,ecx
fffff803`0fc63b17 0f89c0000000    jns     nt!KiSetTimerEx+0x17d (fffff803`0fc63bdd)
fffff803`0fc63b1d 33c9            xor     ecx,ecx
fffff803`0fc63b1f 8bd1            mov     edx,ecx
fffff803`0fc63b21 40f6c5fc        test    bpl,0FCh
fffff803`0fc63b25 0f85a3000000    jne     nt!KiSetTimerEx+0x16e (fffff803`0fc63bce)
fffff803`0fc63b2b 48894c2420      mov     qword ptr [rsp+20h],rcx
fffff803`0fc63b30 48b80800000080f7ffff mov rax,0FFFFF78000000008h
fffff803`0fc63b3a 4d8bc5          mov     r8,r13
fffff803`0fc63b3d 488b00          mov     rax,qword ptr [rax]
fffff803`0fc63b40 804c243340      or      byte ptr [rsp+33h],40h
fffff803`0fc63b45 482bc3          sub     rax,rbx
fffff803`0fc63b48 48894718        mov     qword ptr [rdi+18h],rax
fffff803`0fc63b4c 4803c2          add     rax,rdx
fffff803`0fc63b4f 48c1e812        shr     rax,12h
fffff803`0fc63b53 488bd7          mov     rdx,rdi
fffff803`0fc63b56 440fb6c8        movzx   r9d,al
fffff803`0fc63b5a 44884c2432      mov     byte ptr [rsp+32h],r9b
fffff803`0fc63b5f 8b442430        mov     eax,dword ptr [rsp+30h]
fffff803`0fc63b63 8907            mov     dword ptr [rdi],eax
fffff803`0fc63b65 894f04          mov     dword ptr [rdi+4],ecx
fffff803`0fc63b68 498bce          mov     rcx,r14
fffff803`0fc63b6b e8209ffdff      call    nt!KiInsertTimerTable (fffff803`0fc3da90)
fffff803`0fc63b70 84c0            test    al,al
fffff803`0fc63b72 0f8495000000    je      nt!KiSetTimerEx+0x1ad (fffff803`0fc63c0d)
fffff803`0fc63b78 f7058608510000000200 test dword ptr [nt!PerfGlobalGroupMask+0x8 (fffff803`10174408)],20000h
fffff803`0fc63b82 0f852f2d1a00    jne     nt!KiSetTimerEx+0x1a2e57 (fffff803`0fe068b7)
fffff803`0fc63b88 f081277fffffff  lock and dword ptr [rdi],0FFFFFF7Fh
fffff803`0fc63b8f 488b8424a0000000 mov     rax,qword ptr [rsp+0A0h]
fffff803`0fc63b97 4533c9          xor     r9d,r9d
fffff803`0fc63b9a 33d2            xor     edx,edx
fffff803`0fc63b9c 88442420        mov     byte ptr [rsp+20h],al
fffff803`0fc63ba0 498bce          mov     rcx,r14
fffff803`0fc63ba3 458d4101        lea     r8d,[r9+1]
fffff803`0fc63ba7 e8044efeff      call    nt!KiExitDispatcher (fffff803`0fc489b0)

如上匯編代碼KiSetTimerEx中就是DPC加密細節,如果需要解密只需要逆操作即可,此處我就具體分析下加密細節,分析這個東西我建議你使用記事本帶著色的,

分析思路是這樣的,首先這里要傳入待加密的DPC資料,然后經過KiWaitNeverKiWaitAlways對資料進行xor,ror,bswap等操作,

將解密流程通過代碼的方式實作,

#include <ntddk.h>
#include <ntstrsafe.h>

// 解密DPC
void DPC_Print(PKTIMER ptrTimer)
{
  ULONG_PTR ptrDpc = (ULONG_PTR)ptrTimer->Dpc;
  KDPC* DecDpc = NULL;
  DWORD nShift = (p2dq(ptrKiWaitNever) & 0xFF);

  // _RSI->Dpc = (_KDPC *)v19;
  // _RSI = Timer;
  ptrDpc ^= p2dq(ptrKiWaitNever);        // v19 = KiWaitNever ^ v18;
  ptrDpc = _rotl64(ptrDpc, nShift);      // v18 = __ROR8__((unsigned __int64)Timer ^ _RBX, KiWaitNever);
  ptrDpc ^= (ULONG_PTR)ptrTimer;
  ptrDpc = _byteswap_uint64(ptrDpc);     // __asm { bswap   rbx }
  ptrDpc ^= p2dq(ptrKiWaitAlways);       // _RBX = (unsigned __int64)DPC ^ KiWaitAlways;
  
  // real DPC
  if (MmIsAddressValid((PVOID)ptrDpc))
  {
    DecDpc = (KDPC*)ptrDpc;
    DbgPrint("DPC = %p | routine = %p \n", DecDpc, DecDpc->DeferredRoutine);
  }
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
  DbgPrint("卸載完成... \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
  DbgPrint("hello lyshark.com");
  
  PKTIMER ptrTimer = NULL;
  
  DPC_Print(ptrTimer);

  Driver->DriverUnload = UnDriver;
  return STATUS_SUCCESS;
}

接著將這些功能通過代碼實作,首先得到我們需要的函式地址,這些地址包括,

ULONG_PTR ptrKiProcessorBlock = 0xfffff80770a32cc0;
ULONG_PTR ptrOffsetKTimerTable = 0x3680;
ULONG_PTR ptrKiWaitNever = 0xfffff80770a316f8;
ULONG_PTR ptrKiWaitAlways = 0xfffff80770a318e8;

此處我把它分為三步走,第一步找到KiProcessorBlock函式地址,第二步找到KeSetTimer并從里面尋找KeSetTimerEx,第三步根據KiSetTimerEx地址,搜索到KiWaitNever(),KiWaitAlways()這兩個函式記憶體地址,最侄訓圈鏈表并解密DPC佇列,

第一步: 找到KiProcessorBlock函式地址,該地址可通過__readmsr()暫存器相加偏移得到,

在WinDBG中可以輸入rdmsr c0000082得到MSR地址,

MSR暫存器使用代碼獲取也是很容易,只要找到MSR地址在加上0x20即可得到KiProcessorBlock的地址了,

/*
  lyshark.com 0: kd> dp !KiProcessorBlock
  fffff807`70a32cc0  fffff807`6f77c180 ffffbe81`3cee0180
  fffff807`70a32cd0  00000000`00000000 00000000`00000000
  fffff807`70a32ce0  00000000`00000000 00000000`00000000
  fffff807`70a32cf0  00000000`00000000 00000000`00000000
  fffff807`70a32d00  00000000`00000000 00000000`00000000
  fffff807`70a32d10  00000000`00000000 00000000`00000000
  fffff807`70a32d20  00000000`00000000 00000000`00000000
  fffff807`70a32d30  00000000`00000000 00000000`00000000
*/

#include <ntddk.h>
#include <ntstrsafe.h>

// 得到KiProcessorBlock地址
ULONG64 GetKiProcessorBlock()
{
  ULONG64 PrcbAddress = 0;
  PrcbAddress = (ULONG64)__readmsr(0xC0000101) + 0x20;

  if (PrcbAddress != 0)
  {
    // PrcbAddress 是一個地址 這個地址存放了某個 CPU 的 _KPRCB 的地址
    return *(ULONG_PTR*)PrcbAddress;
  }
  return 0;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
  DbgPrint("卸載完成... \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
  DbgPrint("hello lyshark.com \n");
  
  ULONG64 address = GetKiProcessorBlock();
  if (address != 0)
  {
    DbgPrint("KiProcessorBlock = %p \n", address);
  }

  Driver->DriverUnload = UnDriver;
  return STATUS_SUCCESS;
}

運行后即可得到輸出效果如下:

第二步: 找到KeSetTimer從里面搜索特征得到call KeSetTimerEx函式地址,還記得《驅動開發:內核列舉IoTimer定時器》中我們采用的特征碼定位方式嗎,沒錯本次還要使用這個方法,我們此處需要搜索到e80c000000這段特征,

/*
  lyshark.com 0: kd> uf KeSetTimer
  nt!KeSetTimer:
  fffff807`70520a30 4883ec38        sub     rsp,38h
  fffff807`70520a34 4c89442420      mov     qword ptr [rsp+20h],r8
  fffff807`70520a39 4533c9          xor     r9d,r9d
  fffff807`70520a3c 4533c0          xor     r8d,r8d
  fffff807`70520a3f e80c000000      call    nt!KiSetTimerEx (fffff807`70520a50)
  fffff807`70520a44 4883c438        add     rsp,38h
  fffff807`70520a48 c3              ret
*/

#include <ntddk.h>
#include <ntstrsafe.h>

// 得到KiProcessorBlock地址
ULONG64 GetKeSetTimerEx()
{
  // 獲取 KeSetTimer 地址
  ULONG64 ul_KeSetTimer = 0;
  UNICODE_STRING  uc_KeSetTimer = { 0 };
  RtlInitUnicodeString(&uc_KeSetTimer, L"KeSetTimer");
  ul_KeSetTimer = (ULONG64)MmGetSystemRoutineAddress(&uc_KeSetTimer);
  if (ul_KeSetTimer == 0)
  {
    return 0;
  }

  // 前 30 位元組找 call 指令
  BOOLEAN b_e8 = FALSE;
  ULONG64 ul_e8Addr = 0;

  for (INT i = 0; i < 30; i++)
  {
    // 驗證地址是否可讀寫
    if (!MmIsAddressValid((PVOID64)ul_KeSetTimer))
    { 
      continue;
    }

    // e8 0c 00 00 00 call nt!KiSetTimerEx (fffff807`70520a50)
    if (*(PUCHAR)(ul_KeSetTimer + i) == 0xe8)
    {
      b_e8 = TRUE;
      ul_e8Addr = ul_KeSetTimer + i;
      break;
    }
  }

  // 找到 call 則決議目的地址
  if (b_e8 == TRUE)
  {
    if (!MmIsAddressValid((PVOID64)ul_e8Addr))
    {
      return 0;
    }

    INT ul_callCode = *(INT*)(ul_e8Addr + 1);
    ULONG64 ul_KiSetTimerEx = ul_e8Addr + ul_callCode + 5;
    return ul_KiSetTimerEx;
  }
  return 0;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
  DbgPrint("卸載完成... \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
  DbgPrint("hello lyshark.com \n");

  ULONG64 address = GetKeSetTimerEx();
  if (address != 0)
  {
    DbgPrint("KeSetTimerEx = %p \n", address);
  }

  Driver->DriverUnload = UnDriver;
  return STATUS_SUCCESS;
}

輸出尋找CALL地址效果圖如下:

第三步: 也是最重要的一步,在KiSetTimerEx里面,搜索特征,拿到里面的KiWaitNever(),KiWaitAlways()這兩個函式地址,

  • 488b05850c5100 KiWaitNever
  • 488b356b0e5100 KiWaitAlways

這個程序需要重復搜索,所以要把第一步和第二部程序歸納起來,具體代碼如下所示,

/*
  0: kd> uf KiSetTimerEx
  nt!KiSetTimerEx:
  fffff807`70520a50 48895c2408      mov     qword ptr [rsp+8],rbx
  fffff807`70520a55 48896c2410      mov     qword ptr [rsp+10h],rbp
  fffff807`70520a5a 4889742418      mov     qword ptr [rsp+18h],rsi
  fffff807`70520a5f 57              push    rdi
  fffff807`70520a60 4154            push    r12
  fffff807`70520a62 4155            push    r13
  fffff807`70520a64 4156            push    r14
  fffff807`70520a66 4157            push    r15
  fffff807`70520a68 4883ec50        sub     rsp,50h
  fffff807`70520a6c 488b05850c5100  mov     rax,qword ptr [nt!KiWaitNever (fffff807`70a316f8)]
  fffff807`70520a73 488bf9          mov     rdi,rcx
  fffff807`70520a76 488b356b0e5100  mov     rsi,qword ptr [nt!KiWaitAlways (fffff807`70a318e8)]
  fffff807`70520a7d 410fb6e9        movzx   ebp,r9b
*/

#include <ntddk.h>
#include <ntstrsafe.h>

// 得到KiProcessorBlock地址
ULONG64 GetKeSetTimerEx()
{
  // 獲取 KeSetTimer 地址
  ULONG64 ul_KeSetTimer = 0;
  UNICODE_STRING  uc_KeSetTimer = { 0 };
  RtlInitUnicodeString(&uc_KeSetTimer, L"KeSetTimer");
  ul_KeSetTimer = (ULONG64)MmGetSystemRoutineAddress(&uc_KeSetTimer);
  if (ul_KeSetTimer == 0)
  {
    return 0;
  }

  // 前 30 位元組找 call 指令
  BOOLEAN b_e8 = FALSE;
  ULONG64 ul_e8Addr = 0;

  for (INT i = 0; i < 30; i++)
  {
    // 驗證地址是否可讀寫
    if (!MmIsAddressValid((PVOID64)ul_KeSetTimer))
    { 
      continue;
    }

    // e8 0c 00 00 00 call nt!KiSetTimerEx (fffff807`70520a50)
    if (*(PUCHAR)(ul_KeSetTimer + i) == 0xe8)
    {
      b_e8 = TRUE;
      ul_e8Addr = ul_KeSetTimer + i;
      break;
    }
  }

  // 找到 call 則決議目的地址
  if (b_e8 == TRUE)
  {
    if (!MmIsAddressValid((PVOID64)ul_e8Addr))
    {
      return 0;
    }

    INT ul_callCode = *(INT*)(ul_e8Addr + 1);
    ULONG64 ul_KiSetTimerEx = ul_e8Addr + ul_callCode + 5;
    return ul_KiSetTimerEx;
  }
  return 0;
}

// 得到KiWaitNever地址
ULONG64 GetKiWaitNever(ULONG64 address)
{
  // 驗證地址是否可讀寫
  if (!MmIsAddressValid((PVOID64)address))
  {
    return 0;
  }

  // 前 100 位元組找 找 KiWaitNever
  for (INT i = 0; i < 100; i++)
  {
    // 48 8b 05 85 0c 51 00 | mov rax, qword ptr[nt!KiWaitNever(fffff807`70a316f8)]
    if (*(PUCHAR)(address + i) == 0x48 && *(PUCHAR)(address + i + 1) == 0x8b && *(PUCHAR)(address + i + 2) == 0x05)
    {
      ULONG64 ul_movCode = *(UINT32*)(address + i + 3);
      ULONG64 ul_movAddr = address + i + ul_movCode + 7;
      // DbgPrint("找到KiWaitNever地址: %p \n", ul_movAddr);
      return ul_movAddr;
    }
  }
  return 0;
}

// 得到KiWaitAlways地址
ULONG64 GetKiWaitAlways(ULONG64 address)
{
  // 驗證地址是否可讀寫
  if (!MmIsAddressValid((PVOID64)address))
  {
    return 0;
  }

  // 前 100 位元組找 找 KiWaitNever
  for (INT i = 0; i < 100; i++)
  {
    // 48 8b 35 6b 0e 51 00 | mov rsi,qword ptr [nt!KiWaitAlways (fffff807`70a318e8)]
    if (*(PUCHAR)(address + i) == 0x48 && *(PUCHAR)(address + i + 1) == 0x8b && *(PUCHAR)(address + i + 2) == 0x35)
    {
      ULONG64 ul_movCode = *(UINT32*)(address + i + 3);
      ULONG64 ul_movAddr = address + i + ul_movCode + 7;
      return ul_movAddr;
    }
  }
  return 0;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
  DbgPrint("卸載完成... \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
  DbgPrint("hello lyshark.com \n");

  ULONG64 address = GetKeSetTimerEx();
  if (address != 0)
  {
    ULONG64 KiWaitNeverAddress = GetKiWaitNever(address);
    DbgPrint("KiWaitNeverAddress = %p \n", KiWaitNeverAddress);

    ULONG64 KiWaitAlwaysAddress = GetKiWaitAlways(address);
    DbgPrint("KiWaitAlwaysAddress = %p \n", KiWaitAlwaysAddress);
  }

  Driver->DriverUnload = UnDriver;
  return STATUS_SUCCESS;
}

運行這個程式,我們看下尋找到的地址是否與WinDBG中找到的地址一致,

功能實作部分: 最后將這些功能整合在一起,回圈輸出鏈表元素,并解密元素即可實作列舉當前系統DPC定時器,

代碼核心API分析:

  • KeNumberProcessors 得到CPU數量(內核常量)
  • KeSetSystemAffinityThread 執行緒系結到特定CPU上
  • GetKiProcessorBlock 獲得KPRCB的地址
  • KeRevertToUserAffinityThread 取消系結CPU

解密部分提取出KiWaitNeverKiWaitAlways用于解密計算,轉換PKDPC物件結構,并輸出即可,

#include <Fltkernel.h>
#include <ntddk.h>
#include <intrin.h>

#define IRP_TEST CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

UNICODE_STRING name_device;			// 設備名
UNICODE_STRING name_symbol;			// 符號鏈接
PDEVICE_OBJECT deviceObj;			// 設備物件

typedef struct _KTIMER_TABLE_ENTRY
{
	ULONG_PTR   Lock;
	LIST_ENTRY  Entry;
	ULONG_PTR   Time;
}KTIMER_TABLE_ENTRY, *PKTIMER_TABLE_ENTRY;

typedef struct _KTIMER_TABLE
{
	ULONG_PTR           TimerExpiry[64];
	KTIMER_TABLE_ENTRY  TimerEntries[256];
}KTIMER_TABLE, *PKTIMER_TABLE;

BOOLEAN get_KiWait(PULONG64 never, PULONG64 always)
{
	// 獲取 KeSetTimer 地址
	ULONG64 ul_KeSetTimer = 0;
	UNICODE_STRING	uc_KeSetTimer = { 0 };
	RtlInitUnicodeString(&uc_KeSetTimer, L"KeSetTimer");
	ul_KeSetTimer = (ULONG64)MmGetSystemRoutineAddress(&uc_KeSetTimer);
	if (ul_KeSetTimer == NULL)
	{
		return FALSE;
	}

	// 前 30 位元組找 call 指令
	BOOLEAN b_e8 = FALSE;
	ULONG64 ul_e8Addr = 0;
	for (INT i = 0; i < 30; i++)
	{
		if (!MmIsAddressValid((PVOID64)ul_KeSetTimer))
		{
			continue;
		}

		/*
		0: kd> u nt!KeSetTimer
		nt!KeSetTimer:
		fffff803`0fc63a40 4883ec38        sub     rsp,38h
		fffff803`0fc63a44 4c89442420      mov     qword ptr [rsp+20h],r8
		fffff803`0fc63a49 4533c9          xor     r9d,r9d
		fffff803`0fc63a4c 4533c0          xor     r8d,r8d
		fffff803`0fc63a4f e80c000000      call    nt!KiSetTimerEx (fffff803`0fc63a60)
		fffff803`0fc63a54 4883c438        add     rsp,38h
		fffff803`0fc63a58 c3              ret
		fffff803`0fc63a59 cc              int     3
		*/

		// fffff803`0fc63a4f e8 0c 00 00 00      call    nt!KiSetTimerEx (fffff803`0fc63a60)
		if (*(PUCHAR)(ul_KeSetTimer + i) == 0xe8)
		{
			b_e8 = TRUE;
			ul_e8Addr = ul_KeSetTimer + i;
			break;
		}
	}

	// 找到 call 則決議目的地址
	/*
	0: kd> u nt!KiSetTimerEx l20
	nt!KiSetTimerEx:
	fffff803`0fc63a60 48895c2408      mov     qword ptr [rsp+8],rbx
	fffff803`0fc63a65 48896c2410      mov     qword ptr [rsp+10h],rbp
	fffff803`0fc63a6a 4889742418      mov     qword ptr [rsp+18h],rsi
	fffff803`0fc63a6f 57              push    rdi
	fffff803`0fc63a70 4154            push    r12
	fffff803`0fc63a72 4155            push    r13
	fffff803`0fc63a74 4156            push    r14
	fffff803`0fc63a76 4157            push    r15
	fffff803`0fc63a78 4883ec50        sub     rsp,50h
	fffff803`0fc63a7c 488b057d0c5100  mov     rax,qword ptr [nt!KiWaitNever (fffff803`10174700)]
	fffff803`0fc63a83 488bf9          mov     rdi,rcx
	*/

	ULONG64 ul_KiSetTimerEx = 0;
	if (b_e8 == TRUE)
	{
		if (!MmIsAddressValid((PVOID64)ul_e8Addr))
		{
			return FALSE;
		}
		INT ul_callCode = *(INT*)(ul_e8Addr + 1);
		ULONG64 ul_callAddr = ul_e8Addr + ul_callCode + 5;
		ul_KiSetTimerEx = ul_callAddr;
	}

	// 沒有 call 則直接在當前函式找 
	else
	{
		ul_KiSetTimerEx = ul_KeSetTimer;
	}

	// 前 50 位元組找 找 KiWaitNever 和 KiWaitAlways
	/*
	0: kd> u nt!KiSetTimerEx l20
	nt!KiSetTimerEx:
	fffff803`0fc63a60 48895c2408      mov     qword ptr [rsp+8],rbx
	fffff803`0fc63a65 48896c2410      mov     qword ptr [rsp+10h],rbp
	fffff803`0fc63a6a 4889742418      mov     qword ptr [rsp+18h],rsi
	fffff803`0fc63a6f 57              push    rdi
	fffff803`0fc63a70 4154            push    r12
	fffff803`0fc63a72 4155            push    r13
	fffff803`0fc63a74 4156            push    r14
	fffff803`0fc63a76 4157            push    r15
	fffff803`0fc63a78 4883ec50        sub     rsp,50h
	fffff803`0fc63a7c 488b057d0c5100  mov     rax,qword ptr [nt!KiWaitNever (fffff803`10174700)]
	fffff803`0fc63a83 488bf9          mov     rdi,rcx
	fffff803`0fc63a86 488b35630e5100  mov     rsi,qword ptr [nt!KiWaitAlways (fffff803`101748f0)]
	*/
	if (!MmIsAddressValid((PVOID64)ul_KiSetTimerEx))
	{
		return FALSE;
	}

	ULONG64 ul_arr_ret[2];			// 存放 KiWaitNever 和 KiWaitAlways 的地址
	INT i_sub = 0;					// 對應 ul_arr_ret 的下標
	for (INT i = 0; i < 50; i++)
	{
		// // fffff803`0fc63a7c 488b057d0c5100  mov     rax,qword ptr [nt!KiWaitNever (fffff803`10174700)]
		if (*(PUCHAR)(ul_KiSetTimerEx + i) == 0x48 && *(PUCHAR)(ul_KiSetTimerEx + i + 1) == 0x8b && *(PUCHAR)(ul_KiSetTimerEx + i + 6) == 0x00)
		{
			ULONG64 ul_movCode = *(UINT32*)(ul_KiSetTimerEx + i + 3);
			ULONG64 ul_movAddr = ul_KiSetTimerEx + i + ul_movCode + 7;

			// 只拿符合條件的前兩個值
			if (i_sub < 2)
			{
				ul_arr_ret[i_sub++] = ul_movAddr;
			}
		}
	}
	*never = ul_arr_ret[0];
	*always = ul_arr_ret[1];

	return TRUE;
}

BOOLEAN EnumDpc()
{
	DbgPrint("hello lyshark.com \n");

	// 獲取 CPU 核心數
	INT i_cpuNum = KeNumberProcessors;
	DbgPrint("CPU核心數: %d \n", i_cpuNum);

	for (KAFFINITY i = 0; i < i_cpuNum; i++)
	{
		// 執行緒系結特定 CPU
		KeSetSystemAffinityThread(i + 1);

		// 獲得 KPRCB 的地址
		ULONG64 p_PRCB = (ULONG64)__readmsr(0xC0000101) + 0x20;
		if (!MmIsAddressValid((PVOID64)p_PRCB))
		{
			return FALSE;
		}

		// 取消系結 CPU
		KeRevertToUserAffinityThread();

		// 判斷作業系統版本
		RTL_OSVERSIONINFOEXW OSVersion = { 0 };
		OSVersion.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW);
		RtlGetVersion((PRTL_OSVERSIONINFOW)&OSVersion);

		// 計算 TimerTable 在 _KPRCB 結構中的偏移
		PKTIMER_TABLE p_TimeTable = NULL;
		if (OSVersion.dwMajorVersion == 10 && OSVersion.dwMinorVersion == 0)
		{
			// Windows 10
			p_TimeTable = (PKTIMER_TABLE)(*(PULONG64)p_PRCB + 0x3680);
		}
		else if (OSVersion.dwMajorVersion == 6 && OSVersion.dwMinorVersion == 1)
		{
			// Windows 7
			p_TimeTable = (PKTIMER_TABLE)(*(PULONG64)p_PRCB + 0x2200);
		}
		else
		{
			return FALSE;
		}

		// 遍歷 TimerEntries[] 陣列(大小 256)
		for (INT j = 0; j < 256; j++)
		{
			// 獲取 Entry 雙向鏈表地址
			if (!MmIsAddressValid((PVOID64)p_TimeTable))
			{
				continue;
			}

			PLIST_ENTRY p_ListEntryHead = &(p_TimeTable->TimerEntries[j].Entry);

			// 遍歷 Entry 雙向鏈表
			for (PLIST_ENTRY p_ListEntry = p_ListEntryHead->Flink; p_ListEntry != p_ListEntryHead; p_ListEntry = p_ListEntry->Flink)
			{
				// 根據 Entry 取 _KTIMER 物件地址
				if (!MmIsAddressValid((PVOID64)p_ListEntry))
				{
					continue;
				}

				PKTIMER p_Timer = CONTAINING_RECORD(p_ListEntry, KTIMER, TimerListEntry);

				// 硬編碼取 KiWaitNever 和 KiWaitAlways 
				ULONG64 never = 0, always = 0;
				if (get_KiWait(&never, &always) == FALSE)
				{
					return FALSE;
				}

				// 獲取解密前的 Dpc 物件
				if (!MmIsAddressValid((PVOID64)p_Timer))
				{
					continue;
				}

				ULONG64 ul_Dpc = (ULONG64)p_Timer->Dpc;
				INT i_Shift = (*((PULONG64)never) & 0xFF);

				// 解密 Dpc 物件
				ul_Dpc ^= *((ULONG_PTR*)never);			// 異或
				ul_Dpc = _rotl64(ul_Dpc, i_Shift);		// 回圈左移
				ul_Dpc ^= (ULONG_PTR)p_Timer;			// 異或
				ul_Dpc = _byteswap_uint64(ul_Dpc);		// 顛倒順序
				ul_Dpc ^= *((ULONG_PTR*)always);		// 異或

				// 物件型別轉換
				PKDPC p_Dpc = (PKDPC)ul_Dpc;

				// 列印驗證
				if (!MmIsAddressValid((PVOID64)p_Dpc))
				{
					continue;
				}

				DbgPrint("定時器物件:0x%p | 函式入口:0x%p | 觸發周期: %d \n ", p_Timer, p_Dpc->DeferredRoutine, p_Timer->Period);
			}
		}
	}
	return TRUE;
}

// 對應 IRP_MJ_DEVICE_CONTROL
NTSTATUS myIrpControl(IN PDEVICE_OBJECT pDevObj, IN PIRP pIRP)
{

	// 獲取 IRP 對應的 I/O 堆疊指標
	PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIRP);

	// 得到輸入緩沖區大小
	ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;

	// 得到輸出緩沖區大小
	ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength;

	// 得到 IOCTL 碼
	ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;

	// 捕獲 I/O 操作型別(MajorFunction)
	switch (code)
	{
	case IRP_TEST:
	{
		break;
	}
	default:
		break;
	}

	// 完成 IO 請求
	IoCompleteRequest(pIRP, IO_NO_INCREMENT);

	return STATUS_SUCCESS;
}

// 對應 IRP_MJ_CREATE 、 IRP_MJ_CLOSE
NTSTATUS dpc_CAC(IN PDEVICE_OBJECT pDevObj, IN PIRP pIRP)
{
	// 將 IRP 回傳給 I/O 管理器
	IoCompleteRequest(
		pIRP,				// IRP 指標
		IO_NO_INCREMENT		// 執行緒優先級,IO_NO_INCREMENT :不增加優先級
		);

	// 設定 I/O 請求狀態
	pIRP->IoStatus.Status = STATUS_SUCCESS;

	// 設定 I/O 請求傳輸的位元組數
	pIRP->IoStatus.Information = 0;

	return STATUS_SUCCESS;
}

NTSTATUS CreateDevice(IN PDRIVER_OBJECT DriverObject)
{
	// 定義回傳值
	NTSTATUS status;

	// 初始化設備名
	RtlInitUnicodeString(&name_device, L"\\Device\\LySharkDriver");

	// 創建設備
	status = IoCreateDevice(
		DriverObject,							// 指向驅動物件的指標
		0,										// 設備擴展分配的位元組數
		&name_device,							// 設備名
		FILE_DEVICE_UNKNOWN,					// 設備型別
		0,										// 驅動設備附加資訊
		TRUE,									// 設備物件是否獨占設備
		&deviceObj								// 設備物件指標 
		);

	if (!NT_SUCCESS(status))
	{
		return status;
	}

	// 初始化符號鏈接名
	RtlInitUnicodeString(&name_symbol, L"\\??\\LySharkDriver");

	// 創建符號鏈接
	status = IoCreateSymbolicLink(&name_symbol, &name_device);
	if (!NT_SUCCESS(status))
	{
		return status;
	}

	return STATUS_SUCCESS;
}

NTSTATUS DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
	// 定義回傳值
	NTSTATUS status;

	// 洗掉符號鏈接
	status = IoDeleteSymbolicLink(&name_symbol);
	if (!NT_SUCCESS(status))
	{
		return status;
	}

	// 洗掉設備
	IoDeleteDevice(deviceObj);

	return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
	// 定義回傳值
	NTSTATUS status;

	// 指定驅動卸載函式
	DriverObject->DriverUnload = (PDRIVER_UNLOAD)DriverUnload;

	// 指定派遣函式
	DriverObject->MajorFunction[IRP_MJ_CREATE] = dpc_CAC;
	DriverObject->MajorFunction[IRP_MJ_CLOSE] = dpc_CAC;
	DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = myIrpControl;

	// 創建設備
	status = CreateDevice(DriverObject);
	if (!NT_SUCCESS(status))
	{
		return status;
	}

	// 執行列舉
	EnumDpc();

	return STATUS_SUCCESS;
}

最終運行列舉程式,你將會看到系統中所有的定時器,與ARK工具對比是一致的,

參考文獻: https://www.cnblogs.com/kuangke/p/9397511.html

文章作者:lyshark (王瑞)
文章出處:https://www.cnblogs.com/LyShark/p/16794841.html
著作權宣告:本博客文章與代碼均為學習時整理的筆記,文章 [均為原創] 作品,轉載請 [添加出處] ,您添加出處是我創作的動力!

轉載文章請遵守《中華人民共和國著作權法》相關法律規定或遵守《署名CC BY-ND 4.0國際》規范,合理合規攜帶原創出處轉載!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/514323.html

標籤:C++

上一篇:驅動開發:內核列舉DpcTimer定時器

下一篇:Java注解(3):一個真實的Elasticsearch案例

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more