驅動開發:內核枚舉PspCidTable句柄表

来源:https://www.cnblogs.com/LyShark/archive/2022/10/16/16796158.html
-Advertisement-
Play Games

在上一篇文章`《驅動開發:內核枚舉DpcTimer定時器》`中我們通過枚舉特征碼的方式找到了`DPC`定時器基址並輸出了內核中存在的定時器列表,本章將學習如何通過特征碼定位的方式尋找`Windows 10`系統下麵的`PspCidTable`內核句柄表地址。 ...


在上一篇文章《驅動開發:內核枚舉DpcTimer定時器》中我們通過枚舉特征碼的方式找到了DPC定時器基址並輸出了內核中存在的定時器列表,本章將學習如何通過特征碼定位的方式尋找Windows 10系統下麵的PspCidTable內核句柄表地址。

首先引入一段基礎概念;

  • 1.在windows下所有的資源都是用對象的方式進行管理的(文件、進程、設備等都是對象),當要訪問一個對象時,如打開一個文件,系統就會創建一個對象句柄,通過這個句柄可以對這個文件進行各種操作。
  • 2.句柄和對象的聯繫是通過句柄表來進行的,準確來說一個句柄就是它所對應的對象在句柄表中的索引。
  • 3.通過句柄可以在句柄表中找到對象的指針,通過指針就可以對,對象進行操作。

PspCidTable 就是這樣的一種表(內核句柄表),表的內部存放的是進程EPROCESS線程ETHREAD的內核對象,並通過進程PID線程TID進行索引,ID號以4遞增,內核句柄表不屬於任何進程,也不連接在系統的句柄表上,通過它可以返回系統的任何對象。

內核句柄表與普通句柄表完全一樣,但它與每個進程私有的句柄表有以下不同;

  • 1.PspCidTable 中存放的對象是系統中所有的進程線程對象,其索引就是PIDTID
  • 2.PspCidTable 中存放的直接是對象體EPROCESS和ETHREAD,而每個進程私有的句柄表則存放的是對象頭OBJECT_HEADER
  • 3.PspCidTable 是一個獨立的句柄表,而每個進程私有的句柄表以一個雙鏈連接起來。
  • 4.PspCidTable 訪問對象時要掩掉低三位,每個進程私有的句柄表是雙鏈連接起來的。

那麼在Windows10系統中該如何枚舉句柄表;

  • 1.首先找到PsLookupProcessByProcessId函數地址,該函數是被導出的可以動態拿到。
  • 2.其次在PsLookupProcessByProcessId地址中搜索PspReferenceCidTableEntry函數。
  • 3.最後在PspReferenceCidTableEntry地址中找到PspCidTable函數。

首先第一步先要得到PspCidTable函數記憶體地址,輸入dp PspCidTable即可得到,如果在程式中則是調用MmGetSystemRoutineAddress取到。

PspCidTable是一個HANDLE_TALBE結構,當新建一個進程時,對應的會在PspCidTable存在一個該進程和線程對應的HANDLE_TABLE_ENTRY項。在windows10中依然採用動態擴展的方法,當句柄數少的時候就採用下層表,多的時候才啟用中層表或上層表。

接著我們解析ffffdc88-79605dc0這個記憶體地址,執行dt _HANDLE_TABLE 0xffffdc8879605dc0得到規範化結構體。

內核句柄表分為三層如下;

  • 下層表:是一個HANDLE_TABLE_ENTRY項的索引,整個表共有256個元素,每個元素是一個8個位元組長的HANDLE_TABLE_ENTRY項及索引,HANDLE_TABLE_ENTRY項中保存著指向對象的指針,下層表可以看成是進程和線程的稠密索引。
  • 中層表:共有256個元素,每個元素是4個位元組長的指向下層表的入口指針及索引,中層表可以看成是進程和線程的稀疏索引。
  • 上層表:共有256個元素,每個元素是4個位元組長的指向中層表的入口指針及索引,上層表可以看成是中層表的稀疏索引。

總結起來一個句柄表有一個上層表,一個上層表最多可以有256個中層表的入口指針,每個中層表最多可以有256個下層表的入口指針,每個下層表最多可以有256個進程和線程對象的指針。PspCidTable表可以看成是HANDLE_TBALE_ENTRY項的多級索引。

如上圖所示TableCode是指向句柄表的指針,低二位(二進位)記錄句柄表的等級:0(00)表示一級表,1(01)表示二級表,2(10)表示三級表。這裡的 0xffffdc88-7d09b001 就說名它是一個二級表。

一級表裡存放的就是進程和線程對象(加密過的,需要一些計算來解密),二級表裡存放的是指向某個一級表的指針,同理三級表存放的是指向二級表的指針。

x64 系統中,每張表的大小是 0x1000(4096),一級表中存放的是 _handle_table_entry 結構(大小 = 16),二級表和三級表存放的是指針(大小 = 8)

我們對 0xffffdc88-7d09b001 抹去低二位,輸入dp 0xffffdc887d09b000 輸出的結果就是一張二級表,裡面存儲的就是一級表指針。

繼續查看第一張一級表,輸入dp 0xffffdc887962a000命令,我們知道一級句柄表是根據進程或線程ID來索引的,且以4累加,所以第一行對應id = 0,第二行對應id = 4。根據嘗試,PID = 4的進程是System

所以此處的第二行0xb281de28-3300ffa7就是加密後的System進程的EPROCESS結構,對於Win10系統來說解密演算法(value >> 0x10) & 0xfffffffffffffff0是這樣的,我們通過代碼計算出來。

#include <Windows.h>
#include <iostream>

int _tmain(int argc, _TCHAR* argv[])
{
	std::cout << "hello lyshark.com" << std::endl;

	ULONG64 ul_recode = 0xb281de283300ffa7;

	ULONG64 ul_decode = (LONG64)ul_recode >> 0x10;
	ul_decode &= 0xfffffffffffffff0;

	std::cout << "解密後地址: " << std::hex << ul_decode << std::endl;
	getchar();

	return 0;
}

運行程式得到如下輸出,即可知道System系統進程解密後的EPROCESS結構地址是0xffffb281de283300

回到WinDBG調試器,輸入命令dt _EPROCESS 0xffffb281de283300解析以下這個結構,輸出結果是System進程。

理論知識總結已經結束了,接下來就是如何實現枚舉進程線程了,枚舉流程如下:

  • 1.首先找到PspCidTable的地址。
  • 2.然後找到HANDLE_TBALE的地址。
  • 3.根據TableCode來判斷層次結構。
  • 4.遍歷層次結構來獲取對象地址。
  • 5.判斷對象類型是否為進程對象。
  • 6.判斷進程是否有效。

這裡先來實現獲取PspCidTable函數的動態地址,代碼如下。

#include <ntifs.h>
#include <windef.h>

// 獲取 PspCidTable
// By: LyShark.com
BOOLEAN get_PspCidTable(ULONG64* tableAddr)
{
	// 獲取 PsLookupProcessByProcessId 地址
	UNICODE_STRING uc_funcName;
	RtlInitUnicodeString(&uc_funcName, L"PsLookupProcessByProcessId");
	ULONG64 ul_funcAddr = MmGetSystemRoutineAddress(&uc_funcName);
	if (ul_funcAddr == NULL)
	{
		return FALSE;
	}
	DbgPrint("PsLookupProcessByProcessId addr = %p \n", ul_funcAddr);

	// 前 40 位元組有 call(PspReferenceCidTableEntry)
	/*
	0: kd> uf PsLookupProcessByProcessId
		nt!PsLookupProcessByProcessId:
		fffff802`0841cfe0 48895c2418      mov     qword ptr [rsp+18h],rbx
		fffff802`0841cfe5 56              push    rsi
		fffff802`0841cfe6 4883ec20        sub     rsp,20h
		fffff802`0841cfea 48897c2438      mov     qword ptr [rsp+38h],rdi
		fffff802`0841cfef 488bf2          mov     rsi,rdx
		fffff802`0841cff2 65488b3c2588010000 mov   rdi,qword ptr gs:[188h]
		fffff802`0841cffb 66ff8fe6010000  dec     word ptr [rdi+1E6h]
		fffff802`0841d002 b203            mov     dl,3
		fffff802`0841d004 e887000000      call    nt!PspReferenceCidTableEntry (fffff802`0841d090)
		fffff802`0841d009 488bd8          mov     rbx,rax
		fffff802`0841d00c 4885c0          test    rax,rax
		fffff802`0841d00f 7435            je      nt!PsLookupProcessByProcessId+0x66 (fffff802`0841d046)  Branch
	*/
	ULONG64 ul_entry = 0;
	for (INT i = 0; i < 100; i++)
	{
		// fffff802`0841d004 e8 87 00 00 00      call    nt!PspReferenceCidTableEntry (fffff802`0841d090)
		if (*(PUCHAR)(ul_funcAddr + i) == 0xe8)
		{
			ul_entry = ul_funcAddr + i;
			break;
		}
	}

	if (ul_entry != 0)
	{
		// 解析 call 地址
		INT i_callCode = *(INT*)(ul_entry + 1);
		DbgPrint("i_callCode = %p \n", i_callCode);
		ULONG64 ul_callJmp = ul_entry + i_callCode + 5;
		DbgPrint("ul_callJmp = %p \n", ul_callJmp);

		// 來到 call(PspReferenceCidTableEntry) 內找 PspCidTable
		/*
		0: kd> uf PspReferenceCidTableEntry
			nt!PspReferenceCidTableEntry+0x115:
			fffff802`0841d1a5 488b0d8473f5ff  mov     rcx,qword ptr [nt!PspCidTable (fffff802`08374530)]
			fffff802`0841d1ac b801000000      mov     eax,1
			fffff802`0841d1b1 f0480fc107      lock xadd qword ptr [rdi],rax
			fffff802`0841d1b6 4883c130        add     rcx,30h
			fffff802`0841d1ba f0830c2400      lock or dword ptr [rsp],0
			fffff802`0841d1bf 48833900        cmp     qword ptr [rcx],0
			fffff802`0841d1c3 0f843fffffff    je      nt!PspReferenceCidTableEntry+0x78 (fffff802`0841d108)  Branch
		*/
		for (INT i = 0; i < 0x120; i++)
		{
			// fffff802`0841d1a5 48 8b 0d 84 73 f5 ff  mov     rcx,qword ptr [nt!PspCidTable (fffff802`08374530)]
			if (*(PUCHAR)(ul_callJmp + i) == 0x48 && *(PUCHAR)(ul_callJmp + i + 1) == 0x8b && *(PUCHAR)(ul_callJmp + i + 2) == 0x0d)
			{
				// 解析 mov 地址
				INT i_movCode = *(INT*)(ul_callJmp + i + 3);
				DbgPrint("i_movCode = %p \n", i_movCode);
				ULONG64 ul_movJmp = ul_callJmp + i + i_movCode + 7;
				DbgPrint("ul_movJmp = %p \n", ul_movJmp);

				// 得到 PspCidTable
				*tableAddr = ul_movJmp;
				return TRUE;
			}
		}
	}
	return FALSE;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
	DbgPrint(("Uninstall Driver Is OK \n"));
}

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

	ULONG64 tableAddr = 0;

	get_PspCidTable(&tableAddr);

	DbgPrint("PspCidTable Address = %p \n", tableAddr);

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

運行後即可得到動態地址,我們可以驗證一下是否一致:

繼續增加對與三級表的動態解析代碼,最終代碼如下所示:

#include <ntifs.h>
#include <windef.h>

// 獲取 PspCidTable
// By: LyShark.com
BOOLEAN get_PspCidTable(ULONG64* tableAddr)
{
	// 獲取 PsLookupProcessByProcessId 地址
	UNICODE_STRING uc_funcName;
	RtlInitUnicodeString(&uc_funcName, L"PsLookupProcessByProcessId");
	ULONG64 ul_funcAddr = MmGetSystemRoutineAddress(&uc_funcName);
	if (ul_funcAddr == NULL)
	{
		return FALSE;
	}
	DbgPrint("PsLookupProcessByProcessId addr = %p \n", ul_funcAddr);

	// 前 40 位元組有 call(PspReferenceCidTableEntry)
	/*
	0: kd> uf PsLookupProcessByProcessId
		nt!PsLookupProcessByProcessId:
		fffff802`0841cfe0 48895c2418      mov     qword ptr [rsp+18h],rbx
		fffff802`0841cfe5 56              push    rsi
		fffff802`0841cfe6 4883ec20        sub     rsp,20h
		fffff802`0841cfea 48897c2438      mov     qword ptr [rsp+38h],rdi
		fffff802`0841cfef 488bf2          mov     rsi,rdx
		fffff802`0841cff2 65488b3c2588010000 mov   rdi,qword ptr gs:[188h]
		fffff802`0841cffb 66ff8fe6010000  dec     word ptr [rdi+1E6h]
		fffff802`0841d002 b203            mov     dl,3
		fffff802`0841d004 e887000000      call    nt!PspReferenceCidTableEntry (fffff802`0841d090)
		fffff802`0841d009 488bd8          mov     rbx,rax
		fffff802`0841d00c 4885c0          test    rax,rax
		fffff802`0841d00f 7435            je      nt!PsLookupProcessByProcessId+0x66 (fffff802`0841d046)  Branch
	*/
	ULONG64 ul_entry = 0;
	for (INT i = 0; i < 100; i++)
	{
		// fffff802`0841d004 e8 87 00 00 00      call    nt!PspReferenceCidTableEntry (fffff802`0841d090)
		if (*(PUCHAR)(ul_funcAddr + i) == 0xe8)
		{
			ul_entry = ul_funcAddr + i;
			break;
		}
	}

	if (ul_entry != 0)
	{
		// 解析 call 地址
		INT i_callCode = *(INT*)(ul_entry + 1);
		DbgPrint("i_callCode = %p \n", i_callCode);
		ULONG64 ul_callJmp = ul_entry + i_callCode + 5;
		DbgPrint("ul_callJmp = %p \n", ul_callJmp);

		// 來到 call(PspReferenceCidTableEntry) 內找 PspCidTable
		/*
		0: kd> uf PspReferenceCidTableEntry
			nt!PspReferenceCidTableEntry+0x115:
			fffff802`0841d1a5 488b0d8473f5ff  mov     rcx,qword ptr [nt!PspCidTable (fffff802`08374530)]
			fffff802`0841d1ac b801000000      mov     eax,1
			fffff802`0841d1b1 f0480fc107      lock xadd qword ptr [rdi],rax
			fffff802`0841d1b6 4883c130        add     rcx,30h
			fffff802`0841d1ba f0830c2400      lock or dword ptr [rsp],0
			fffff802`0841d1bf 48833900        cmp     qword ptr [rcx],0
			fffff802`0841d1c3 0f843fffffff    je      nt!PspReferenceCidTableEntry+0x78 (fffff802`0841d108)  Branch
		*/
		for (INT i = 0; i < 0x120; i++)
		{
			// fffff802`0841d1a5 48 8b 0d 84 73 f5 ff  mov     rcx,qword ptr [nt!PspCidTable (fffff802`08374530)]
			if (*(PUCHAR)(ul_callJmp + i) == 0x48 && *(PUCHAR)(ul_callJmp + i + 1) == 0x8b && *(PUCHAR)(ul_callJmp + i + 2) == 0x0d)
			{
				// 解析 mov 地址
				INT i_movCode = *(INT*)(ul_callJmp + i + 3);
				DbgPrint("i_movCode = %p \n", i_movCode);
				ULONG64 ul_movJmp = ul_callJmp + i + i_movCode + 7;
				DbgPrint("ul_movJmp = %p \n", ul_movJmp);

				// 得到 PspCidTable
				*tableAddr = ul_movJmp;
				return TRUE;
			}
		}
	}
	return FALSE;
}

/* 解析一級表
// By: LyShark.com
BaseAddr:一級表的基地址
index1:第幾個一級表
index2:第幾個二級表
*/
VOID parse_table_1(ULONG64 BaseAddr, INT index1, INT index2)
{
	// 遍歷一級表(每個表項大小 16 ),表大小 4k,所以遍歷 4096/16 = 526 次
	PEPROCESS p_eprocess = NULL;
	PETHREAD p_ethread = NULL;
	INT i_id = 0;
	for (INT i = 0; i < 256; i++)
	{
		if (!MmIsAddressValid((PVOID64)(BaseAddr + i * 16)))
		{
			DbgPrint("非法地址= %p \n", BaseAddr + i * 16);
			continue;
		}

		ULONG64 ul_recode = *(PULONG64)(BaseAddr + i * 16);
		
		// 解密
		ULONG64 ul_decode = (LONG64)ul_recode >> 0x10;
		ul_decode &= 0xfffffffffffffff0;
		
		// 判斷是進程還是線程
		i_id = i * 4 + 1024 * index1 + 512 * index2 * 1024;
		if (PsLookupProcessByProcessId(i_id, &p_eprocess) == STATUS_SUCCESS)
		{
			DbgPrint("進程PID: %d | ID: %d | 記憶體地址: %p | 對象: %p \n", i_id, i, BaseAddr + i * 0x10, ul_decode);
		}
		else if (PsLookupThreadByThreadId(i_id, &p_ethread) == STATUS_SUCCESS)
		{
			DbgPrint("線程TID: %d | ID: %d | 記憶體地址: %p | 對象: %p \n", i_id, i, BaseAddr + i * 0x10, ul_decode);
		}
	}
}

/* 解析二級表
// By: LyShark.com
BaseAddr:二級表基地址
index2:第幾個二級表
*/
VOID parse_table_2(ULONG64 BaseAddr, INT index2)
{
	// 遍歷二級表(每個表項大小 8),表大小 4k,所以遍歷 4096/8 = 512 次
	ULONG64 ul_baseAddr_1 = 0;
	for (INT i = 0; i < 512; i++)
	{
		if (!MmIsAddressValid((PVOID64)(BaseAddr + i * 8)))
		{
			DbgPrint("非法二級表指針(1):%p \n", BaseAddr + i * 8);
			continue;
		}
		if (!MmIsAddressValid((PVOID64)*(PULONG64)(BaseAddr + i * 8)))
		{
			DbgPrint("非法二級表指針(2):%p \n", BaseAddr + i * 8);
			continue;
		}
		ul_baseAddr_1 = *(PULONG64)(BaseAddr + i * 8);
		parse_table_1(ul_baseAddr_1, i, index2);
	}
}

/* 解析三級表
// By: LyShark.com
BaseAddr:三級表基地址
*/
VOID parse_table_3(ULONG64 BaseAddr)
{
	// 遍歷三級表(每個表項大小 8),表大小 4k,所以遍歷 4096/8 = 512 次
	ULONG64 ul_baseAddr_2 = 0;
	for (INT i = 0; i < 512; i++)
	{
		if (!MmIsAddressValid((PVOID64)(BaseAddr + i * 8)))
		{
			continue;
		}
		if (!MmIsAddressValid((PVOID64)* (PULONG64)(BaseAddr + i * 8)))
		{
			continue;
		}
		ul_baseAddr_2 = *(PULONG64)(BaseAddr + i * 8);
		parse_table_2(ul_baseAddr_2, i);
	}
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
	DbgPrint(("Uninstall Driver Is OK \n"));
}

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

	ULONG64 tableAddr = 0;

	get_PspCidTable(&tableAddr);

	DbgPrint("PspCidTable Address = %p \n", tableAddr);

	// 獲取 _HANDLE_TABLE 的 TableCode
	ULONG64 ul_tableCode = *(PULONG64)(((ULONG64)*(PULONG64)tableAddr) + 8);
	DbgPrint("ul_tableCode = %p \n", ul_tableCode);

	// 取低 2位(二級制11 = 3)
	INT i_low2 = ul_tableCode & 3;
	DbgPrint("i_low2 = %X \n", i_low2);

	// 一級表
	if (i_low2 == 0)
	{
		// TableCode 低 2位抹零(二級制11 = 3)
		parse_table_1(ul_tableCode & (~3), 0, 0);
	}
	// 二級表
	else if (i_low2 == 1)
	{
		// TableCode 低 2位抹零(二級制11 = 3)
		parse_table_2(ul_tableCode & (~3), 0);
	}
	// 三級表
	else if (i_low2 == 2)
	{
		// TableCode 低 2位抹零(二級制11 = 3)
		parse_table_3(ul_tableCode & (~3));
	}
	else
	{
		DbgPrint("LyShark提示: 錯誤,非法! ");
		return FALSE;
	}

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

運行如上完整代碼,我們可以在WinDBG中捕捉到枚舉到的進程信息:

線程信息在進程信息的下麵,枚舉效果如下:

至此文章就結束了,這裡多說一句,實際上ZwQuerySystemInformation枚舉系統句柄時就是走的這條雙鏈,枚舉系統進程如果使用的是這個API函數,那麼不出意外它也是在這些內核表中做的解析。

參考文獻

http://www.blogfshare.com/details-in-pspcidtbale.html
https://blog.csdn.net/whatday/article/details/17189093
https://www.cnblogs.com/kuangke/p/5761615.html

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

轉載文章請遵守《中華人民共和國著作權法》相關法律規定或遵守《署名CC BY-ND 4.0國際》規範,合理合規攜帶原創出處轉載!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 背景介紹: 最近在搭建一個公共項目,類似業務操作記錄上報的功能,就想著給業務方提供統一的sdk,在sdk中實現客戶端和服務端的交互封裝,對業務方幾乎是無感的。訪問關係如下圖: 訪問關係示意圖 這裡採用了http的方式進行交互,但是,如果每次介面調用都需要感知http的封裝,一來代碼重覆度較高,二來新 ...
  • JDBC和連接池04 10.資料庫連接池 10.1傳統連接弊端分析 傳統獲取Connection問題分析 傳統的 JDBC 資料庫連接使用DriverManager來獲取,每次向資料庫建立連接的時候都要將Connection載入到記憶體中,再驗證IP地址,用戶名和密碼(約0.05s~1s時間)。需要數 ...
  • 參考文檔:https://spdlog.docsforge.com/master/ spdlog簡介 Very fast, header only, C++ logging library. 一個header-only的C++日誌庫,十分高效且易用。 獲取安裝方式 https://github.co ...
  • 這篇文章比較特殊,是一篇穿插答疑文章,由於剛好在前一篇教程`《驅動開發:內核枚舉PspCidTable句柄表》`整理了枚舉句柄表的知識點,正好這個知識點能解決一個問題,事情是這樣的有一個粉絲求助了一個問題,想要枚舉處驅動中活動的線程信息,此功能我並沒有嘗試過當時也只是說了一個大致思路,今天想具體聊一... ...
  • explicit的意義是讓程式員能制止"單一參數的constructor"被當作conversion運算符 default constructor default constructor只有在被編譯器需要時,才會被合成出來,且合成出的constructor只執行編譯器所需要的行動(並不對成員初始化) ...
  • ##MybatisPlus生成主鍵策略方法 ###全局id生成策略【因為是全局id所以不推薦】 SpringBoot集成Mybatis-Plus 在yaml配置文件中添加MP配置 mybatis-plus: global-config: db-config: #主鍵類型(auto:"自增id",as ...
  • 首先聲明,本文參照(7條消息) 【中文】【吳恩達課後編程作業】Course 1 - 神經網路和深度學習 - 第三周作業_何寬的博客-CSDN博客_吳恩達課後編程作業(https://blog.csdn.net/u013733326/article/details/79702148) 本文所使用的資料 ...
  • 上一章講到利用路由器鏡像的功能轉發消息,本章介紹物聯網終端的另一應用場景——通過智能終端收發QQ消息。 硬體準備 (無) 環境搭建 實現QQ消息轉發需要依賴社區維護的QQ客戶端gocqhttp以及聊天機器人框架nonebot2,而在這個社區內fubuki-iot是作為一個插件的形式存在的。因此完整的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...