什么是中断?

 指当出现需要时,CPU暂时停止当前程序的执行转而执行处理新情况的程序和执行过程。即在程序运行过程中,系统出现了一个必须由CPU立即处理的情况,此时,CPU暂时中止程序的执行转而处理这个新的情况的过程就叫做中断。

比如:除零(0号中断)、断点(3号中断)、系统调用(2e号中断)、以及异常处理等都会引发中断,所以自然需要相应的中断例程去进行处理。

这样操作系统就会用数据结构来维护这些中断例程,这个数据结构就是IDT(Interrupt Descriptor Table)。

中断描述表

IDT表的长度与地址是由CPU的IDTR寄存器来描述的。IDTR寄存器共有48位,高32位是IDT表的基地址,低16位是IDT的长度。

typedef struct _IDTR{
USHORT IDT_limit;
USHORT IDT_LOWbase;
USHORT IDT_HIGbase;
}IDTR,*PIDTR; IDTR idtr; __asm SIDT idtr;

可以通过以上SIDT指令可以读取IDTR寄存器。然后通过MAKEWORD宏把高位与地位组合起来就可以获得IDT表的基地址了。

简单来说,IDT表是一张位于物理内存的线性表,共有256个表项。在32位模式下,每个IDT表项的长度是8个字节(64 bit),IDT表的总长度是2048字节。

kd> r idtr
idtr=8003f400
kd> r idtl
idtl=000007ff

通过Windbg命令 r idtr、r idtl可以读取IDT表的基地址与边界。

如图可以清晰的看见每一个表项了,可是每一个表项8字节都代表什么意思呢?

IDT表中每一项也称为“门描述符”,之所以这样称呼,是因为IDT表项的基本用途就是引领CPU从一个空间到另一个空间去执行,每个表项好像是一个空间到另一个空间的大门。

IDT表中可以包含以下3种门描述符:

任务门描述符:用于任务切换,里面包含用于选择任务状态段(TSS)的段选择子。可以使用JMP或CALL指令通过任务门来切换到任务门所指向的任务,当CPU因为中断或异常转移到任务门时,也会切换到指定任务。

中断门描述符:用于描述中断例程的入口。

陷阱门描述符:用于描述异常处理例程的入口。

以下为三种门描述符的内存布局:

结构体定义为

typedef struct _IDTENTRY
{
unsigned short LowOffset;
unsigned short selector;
unsigned char retention:5;
unsigned char zero1:3;
unsigned char gate_type:1;
unsigned char zero2:1;
unsigned char interrupt_gate_size:1;
unsigned char zero3:1;
unsigned char zero4:1;
unsigned char DPL:2;
unsigned char P:1;
unsigned short HiOffset;
} IDTENTRY,*PIDTENTRY;

其中DPL代表描述符优先级,用于优先级控制,P是段存在标志,段选择子用来选择一个段描述符(LDT或GDT)偏移部分用来指定段中偏移。两者共同准确的定义一个内存地址,对于中断门和陷阱门,他们指定的就是中断或异常处理例程的地址,对于任务门它们指定的就是任务状态段地址。

也就是说段选择子提供一个所谓的段基地址,那么处理例程 = 段基址 + 段偏移。

  那么HOOK方式就有两种了:更改段偏移或者更改段基址。

  我们先忽略段选择子提供的段基址,先简单的认为门描述符提供的高16位和低16位的段内偏移就是该中断的处理例程(其实本来就是,因为32位操作系统中,已经弱化了段基址的概念,运用了平坦模型,也就是说段基址就是0)。

然后我们看看HOOK代码:

#ifndef CXX_IDTHOOK_H
# include "IDTHook.h"
#endif #define WORD USHORT
#define DWORD ULONG ULONG g_InterruptFun = 0; #define MAKELONG(a, b) ((LONG)(((WORD)(((DWORD_PTR)(a)) & 0xffff)) \
| ((DWORD)((WORD)(((DWORD_PTR)(b)) & 0xffff))) << 16)) NTKERNELAPI VOID KeSetSystemAffinityThread ( KAFFINITY Affinity );
NTKERNELAPI VOID KeRevertToUserAffinityThread ( VOID ); PULONG GetKiProcessorBlock()
{
ULONG* KiProcessorBlock = 0; KeSetSystemAffinityThread(1); //使当前线程运行在第一个处理器上 _asm
{
push eax
mov eax,FS:[0x34]
add eax,20h
mov eax,[eax]
mov eax,[eax]
mov eax,[eax+218h]
mov KiProcessorBlock,eax
pop eax
} KeRevertToUserAffinityThread(); return KiProcessorBlock ; }
void PageProtectOn()
{
__asm{//恢复内存保护
mov eax,cr0
or eax,10000h
mov cr0,eax
sti
}
} void PageProtectOff()
{
__asm{//去掉内存保护
cli
mov eax,cr0
and eax,not 10000h
mov cr0,eax
}
} void _stdcall FilterInterruptFun()
{
DbgPrint("CurrentProcess : %s",(char*)PsGetCurrentProcess()+0x174);
} _declspec(naked)
void Fake_InterruptFun()
{
_asm{
pushad
pushfd push fs
push 0x30
pop fs call FilterInterruptFun;
pop fs popfd
popad jmp g_InterruptFun
}
}; NTSTATUS
DriverEntry(IN PDRIVER_OBJECT pDriverObj, IN PUNICODE_STRING pRegistryString)
{
IDTR Idtr;
PIDTENTRY pIdtEntry;
ULONG ulIndex = 0 ;
ULONG* KiProcessorBlock; pDriverObj->DriverUnload = DriverUnload; KiProcessorBlock = GetKiProcessorBlock(); DbgPrint("%X\r\n",KiProcessorBlock); while (KiProcessorBlock[ulIndex])
{
pIdtEntry = *(PIDTENTRY*)(KiProcessorBlock[ulIndex] - 0x120 + 0x38) ; DbgPrint("IDT Base:%X\r\n",pIdtEntry); g_InterruptFun = MAKELONG(pIdtEntry[3].LowOffset,pIdtEntry[3].HiOffset); DbgPrint("InterruptFun3:%X\r\n",g_InterruptFun); PageProtectOff();
pIdtEntry[3].LowOffset = (unsigned short)((ULONG)Fake_InterruptFun & 0xffff);
pIdtEntry[3].HiOffset = (unsigned short)((ULONG)Fake_InterruptFun >> 16);
PageProtectOn(); ulIndex++;
} return STATUS_SUCCESS;
} VOID
DriverUnload(IN PDRIVER_OBJECT pDriverObj)
{ return;
}

稍微解释一下,里面获得IDT表的时候没有通过寄存器IDTR进行读取,是因为对于多核CPU来说不一定只有一个IDT表,而通过IDTR来读取只能读到一份表,所以HOOK IDT的时候一定要注意多核问题

系统维护了一个全局的处理器数组KiProcessorBlock,其中每个元素对应于一个处理器的KPRCB 对象。

kd> dt _KPCR
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x01c SelfPcr : Ptr32 _KPCR
+0x020 Prcb : Ptr32 _KPRCB
+0x024 Irql : UChar
+0x028 IRR : Uint4B
+0x02c IrrActive : Uint4B
+0x030 IDR : Uint4B
+0x034 KdVersionBlock : Ptr32 Void
+0x038 IDT : Ptr32 _KIDTENTRY
+0x03c GDT : Ptr32 _KGDTENTRY
+0x040 TSS : Ptr32 _KTSS
+0x044 MajorVersion : Uint2B
+0x046 MinorVersion : Uint2B
+0x048 SetMember : Uint4B
+0x04c StallScaleFactor : Uint4B
+0x050 DebugActive : UChar
+0x051 Number : UChar
+0x052 Spare0 : UChar
+0x053 SecondLevelCacheAssociativity : UChar
+0x054 VdmAlert : Uint4B
+0x058 KernelReserved : [14] Uint4B
+0x090 SecondLevelCacheSize : Uint4B
+0x094 HalReserved : [16] Uint4B
+0x0d4 InterruptMode : Uint4B
+0x0d8 Spare1 : UChar
+0x0dc KernelReserved2 : [17] Uint4B
+0x120 PrcbData : _KPRCB

在0X120处,有个_KPRCB结构。再来看看这个结构。

kd> dt _KPCR
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x01c SelfPcr : Ptr32 _KPCR
+0x020 Prcb : Ptr32 _KPRCB
+0x024 Irql : UChar
+0x028 IRR : Uint4B
+0x02c IrrActive : Uint4B
+0x030 IDR : Uint4B
+0x034 KdVersionBlock : Ptr32 Void
+0x038 IDT : Ptr32 _KIDTENTRY
+0x03c GDT : Ptr32 _KGDTENTRY
+0x040 TSS : Ptr32 _KTSS
+0x044 MajorVersion : Uint2B
+0x046 MinorVersion : Uint2B
+0x048 SetMember : Uint4B
+0x04c StallScaleFactor : Uint4B
+0x050 DebugActive : UChar
+0x051 Number : UChar
+0x052 Spare0 : UChar
+0x053 SecondLevelCacheAssociativity : UChar
+0x054 VdmAlert : Uint4B
+0x058 KernelReserved : [14] Uint4B
+0x090 SecondLevelCacheSize : Uint4B
+0x094 HalReserved : [16] Uint4B
+0x0d4 InterruptMode : Uint4B
+0x0d8 Spare1 : UChar
+0x0dc KernelReserved2 : [17] Uint4B
+0x120 PrcbData : _KPRCB
kd> dt _PRCB
Symbol _PRCB not found.
kd> dt _KPRCB
nt!_KPRCB
+0x000 MinorVersion : Uint2B
+0x002 MajorVersion : Uint2B
+0x004 CurrentThread : Ptr32 _KTHREAD
+0x008 NextThread : Ptr32 _KTHREAD
+0x00c IdleThread : Ptr32 _KTHREAD
+0x010 Number : Char
+0x011 Reserved : Char
+0x012 BuildType : Uint2B
+0x014 SetMember : Uint4B
+0x018 CpuType : Char
+0x019 CpuID : Char
+0x01a CpuStep : Uint2B
+0x01c ProcessorState : _KPROCESSOR_STATE
+0x33c KernelReserved : [16] Uint4B
+0x37c HalReserved : [16] Uint4B
+0x3bc PrcbPad0 : [92] UChar
+0x418 LockQueue : [16] _KSPIN_LOCK_QUEUE
+0x498 PrcbPad1 : [8] UChar
+0x4a0 NpxThread : Ptr32 _KTHREAD
+0x4a4 InterruptCount : Uint4B
+0x4a8 KernelTime : Uint4B
+0x4ac UserTime : Uint4B
+0x4b0 DpcTime : Uint4B
+0x4b4 DebugDpcTime : Uint4B
+0x4b8 InterruptTime : Uint4B
+0x4bc AdjustDpcThreshold : Uint4B
+0x4c0 PageColor : Uint4B
+0x4c4 SkipTick : Uint4B
+0x4c8 MultiThreadSetBusy : UChar
+0x4c9 Spare2 : [3] UChar
+0x4cc ParentNode : Ptr32 _KNODE
+0x4d0 MultiThreadProcessorSet : Uint4B
+0x4d4 MultiThreadSetMaster : Ptr32 _KPRCB
+0x4d8 ThreadStartCount : [2] Uint4B
+0x4e0 CcFastReadNoWait : Uint4B
+0x4e4 CcFastReadWait : Uint4B
+0x4e8 CcFastReadNotPossible : Uint4B
+0x4ec CcCopyReadNoWait : Uint4B
+0x4f0 CcCopyReadWait : Uint4B
+0x4f4 CcCopyReadNoWaitMiss : Uint4B
+0x4f8 KeAlignmentFixupCount : Uint4B
+0x4fc KeContextSwitches : Uint4B
+0x500 KeDcacheFlushCount : Uint4B
+0x504 KeExceptionDispatchCount : Uint4B
+0x508 KeFirstLevelTbFills : Uint4B
+0x50c KeFloatingEmulationCount : Uint4B
+0x510 KeIcacheFlushCount : Uint4B
+0x514 KeSecondLevelTbFills : Uint4B
+0x518 KeSystemCalls : Uint4B
+0x51c SpareCounter0 : [1] Uint4B
+0x520 PPLookasideList : [16] _PP_LOOKASIDE_LIST
+0x5a0 PPNPagedLookasideList : [32] _PP_LOOKASIDE_LIST
+0x6a0 PPPagedLookasideList : [32] _PP_LOOKASIDE_LIST
+0x7a0 PacketBarrier : Uint4B
+0x7a4 ReverseStall : Uint4B
+0x7a8 IpiFrame : Ptr32 Void
+0x7ac PrcbPad2 : [52] UChar
+0x7e0 CurrentPacket : [3] Ptr32 Void
+0x7ec TargetSet : Uint4B
+0x7f0 WorkerRoutine : Ptr32 void
+0x7f4 IpiFrozen : Uint4B
+0x7f8 PrcbPad3 : [40] UChar
+0x820 RequestSummary : Uint4B
+0x824 SignalDone : Ptr32 _KPRCB
+0x828 PrcbPad4 : [56] UChar
+0x860 DpcListHead : _LIST_ENTRY
+0x868 DpcStack : Ptr32 Void
+0x86c DpcCount : Uint4B
+0x870 DpcQueueDepth : Uint4B
+0x874 DpcRoutineActive : Uint4B
+0x878 DpcInterruptRequested : Uint4B
+0x87c DpcLastCount : Uint4B
+0x880 DpcRequestRate : Uint4B
+0x884 MaximumDpcQueueDepth : Uint4B
+0x888 MinimumDpcRate : Uint4B
+0x88c QuantumEnd : Uint4B
+0x890 PrcbPad5 : [16] UChar
+0x8a0 DpcLock : Uint4B
+0x8a4 PrcbPad6 : [28] UChar
+0x8c0 CallDpc : _KDPC
+0x8e0 ChainedInterruptList : Ptr32 Void
+0x8e4 LookasideIrpFloat : Int4B
+0x8e8 SpareFields0 : [6] Uint4B
+0x900 VendorString : [13] UChar
+0x90d InitialApicId : UChar
+0x90e LogicalProcessorsPerPhysicalProcessor : UChar
+0x910 MHz : Uint4B
+0x914 FeatureBits : Uint4B
+0x918 UpdateSignature : _LARGE_INTEGER
+0x920 NpxSaveArea : _FX_SAVE_AREA
+0xb30 PowerState : _PROCESSOR_POWER_STATE

在0x38的地方是不是看到了我们熟悉的IDT表。

我们在Windbg下dd KiProcessBlock

kd> dd KiProcessorBlock
80553e40 ffdff120 00000000 00000000 00000000
80553e50 00000000 00000000 00000000 00000000
80553e60 00000000 00000000 00000000 00000000
80553e70 00000000 00000000 00000000 00000000

因为是虚拟机里面做的测试,所以只是单核CPU,那么问题来了,如何获得KiProcessBlock?

在Win732位与XP中它是未导出的,那么可以用IDA在ntosknl导出表搜索,找到哪个函数中用了这个变量就可以用这个函数加硬编码的方式进行强行定位,上次看到一篇帖子是如何获得系统未导出的全局变量,是通过遍历未公开的结构体,我就试了试,没想到还成功了,然后就用了这种方法,其实解决多核问题还有好多种方法,不一定局限于哪一种。

下面着重讲解下,如何修改段基址的方法实现HOOK,然后躲过xuetr的检测。

IA-32处理器有三种描述符表:全局描述符表GDT,局部描述符表LDT,中断描述符表IDT。

GDT表是全局的,一个系统中通常只有一个GDT表,供系统中所有程序和任务进行使用。LDT与任务相关,每个任务可以有一个LDT,也可以让多个任务共享一个LDT。

我们用WINDBG观察下GDT

kd> r gdtr
gdtr=8003f000

获得GDT的基地址之后观察下它的内存

是不是和IDT表一样啊,这时候里面的一项叫做段描述符。

这个图就是段描述符的内存结构。有点看不清,将就一下,详情可以参考张银奎老师的《软件调试》。

typedef struct _KGDTENTRY {
USHORT LimitLow;
USHORT BaseLow;
union {
struct {
UCHAR BaseMid;
UCHAR Flags1; // Declare as bytes to avoid alignment
UCHAR Flags2; // Problems.
UCHAR BaseHi;
} Bytes;
struct {
ULONG BaseMid : 8;
ULONG Type : 5;
ULONG Dpl : 2;
ULONG Pres : 1; ULONG LimitHi : 4;
ULONG Sys : 1;
ULONG Reserved_0 : 1;
ULONG Default_Big : 1;
ULONG Granularity : 1;
ULONG BaseHi : 8;
} Bits;
} HighWord;
} KGDTENTRY, *PKGDTENTRY;

段选择子:

 还记得门描述符里的选择子吗?选择子的作用就是选择一个段描述符,相当于索引。

段选择子的T1位代表要索引的段描述符表,T1=0表示全局描述符表,T1=1表示局部描述符表。

段选择子的高13位是描述符索引,即要选择的段描述符在T1所表示的段描述符表中的索引号。因为这里使用的是13位,意味着最多可索引8192个描述符,所以GDT和LDT表的最大表项数都是8192.因为X86CPU最多支持256个中断向量,所以IDT 表的最多表项数是256.

然后我们来看看如何修改段基址来实现IDT HOOK。

要是段内偏移不变,那么必须满足:原始偏移 + newbase = newfuntion

那么newbase  = newfuntion - 原始偏移  


然后把newbase分解了再填到段描述符中就可以了吗?答案是不可以,因为一个段描述符可能会有很多程序在用,如果无故修改段描述符那么就会产生不可预料的错误,那么我们如何修改段基址呢?

我们可以变通一下,因为段描述符表里有很多空的未利用的,我们可以把相应的门描述符里的段选择子改掉,让它去选择一个空的段描述符,我们把原先段选择符内容拷贝过来再修改这个段描述符就可以达到目的了!

#include "ntifs.h"

#define WORD    USHORT
#define DWORD ULONG #define MAKELONG(a, b) ((LONG)(((WORD)(((DWORD_PTR)(a)) & 0xffff)) \
| ((DWORD)((WORD)(((DWORD_PTR)(b)) & 0xffff))) << 16)) typedef struct _IDTR{
USHORT IDT_limit;
USHORT IDT_LOWbase;
USHORT IDT_HIGbase;
}IDTR,*PIDTR; typedef struct _IDTENTRY
{
unsigned short LowOffset;
unsigned short selector;
unsigned char retention:5;
unsigned char zero1:3;
unsigned char gate_type:1;
unsigned char zero2:1;
unsigned char interrupt_gate_size:1;
unsigned char zero3:1;
unsigned char zero4:1;
unsigned char DPL:2;
unsigned char P:1;
unsigned short HiOffset;
} IDTENTRY,*PIDTENTRY; typedef struct _KGDTENTRY {
USHORT LimitLow;
USHORT BaseLow;
union {
struct {
UCHAR BaseMid;
UCHAR Flags1; // Declare as bytes to avoid alignment
UCHAR Flags2; // Problems.
UCHAR BaseHi;
} Bytes;
struct {
ULONG BaseMid : 8;
ULONG Type : 5;
ULONG Dpl : 2;
ULONG Pres : 1; ULONG LimitHi : 4;
ULONG Sys : 1;
ULONG Reserved_0 : 1;
ULONG Default_Big : 1;
ULONG Granularity : 1;
ULONG BaseHi : 8;
} Bits;
} HighWord;
} KGDTENTRY, *PKGDTENTRY; //global
USHORT g_FilterJmp[3];
ULONG g_uOrigInterruptFunc; void PageProtectOn()
{
__asm{//恢复内存保护
mov eax,cr0
or eax,10000h
mov cr0,eax
sti
}
} void PageProtectOff()
{
__asm{//去掉内存保护
cli
mov eax,cr0
and eax,not 10000h
mov cr0,eax
}
} USHORT g_u_cs; void __stdcall FilterInterrupt()
{
KdPrint(("%s---%X",(char*)PsGetCurrentProcess()+0x16c,g_u_cs));
} __declspec(naked)
void NewInterrupt3OfOrigBase()
{
__asm{
pushad
pushfd push fs
push 0x30
pop fs call FilterInterrupt pop fs popfd
popad jmp g_uOrigInterruptFunc
}
} __declspec(naked)
void NewInterrupt3()
{
__asm{
mov g_u_cs,cs
jmp fword ptr[g_FilterJmp]
}
} ULONG GetInterruptFuncAddress(ULONG InterruptIndex)
{
IDTR idtr;
IDTENTRY *pIdtEntry; __asm SIDT idtr; pIdtEntry = (IDTENTRY *)MAKELONG(idtr.IDT_LOWbase,idtr.IDT_HIGbase); return MAKELONG(pIdtEntry[InterruptIndex].LowOffset,pIdtEntry[InterruptIndex].HiOffset);
} ULONG GetNewBase(ULONG NewInterruptFunc,ULONG OrigInterruptOffset)
{
return (NewInterruptFunc - OrigInterruptOffset);
} VOID SetInterrupt(ULONG InterruptIndex,ULONG uNewBase,BOOLEAN bIsNew)
{
ULONG u_fnKeSetTimeIncrement;
UNICODE_STRING usFuncName;
ULONG u_index;
ULONG *u_KiProcessorBlock; IDTENTRY *pIdtEntry;
PKGDTENTRY pGdt; RtlInitUnicodeString(&usFuncName,L"KeSetTimeIncrement"); u_fnKeSetTimeIncrement = (ULONG)MmGetSystemRoutineAddress(&usFuncName);
if (!MmIsAddressValid((PVOID)u_fnKeSetTimeIncrement))
{
return;
} u_KiProcessorBlock = *(ULONG**)(u_fnKeSetTimeIncrement + 44); u_index = 0;
while (u_KiProcessorBlock[u_index])
{
pIdtEntry = *(IDTENTRY**)(u_KiProcessorBlock[u_index] - 0xE8);
pGdt = *(PKGDTENTRY*)(u_KiProcessorBlock[u_index] - 0xE4); PageProtectOff(); if (bIsNew)
{
pIdtEntry[InterruptIndex].selector = 0xA8; //10101 000 //低1 2位 RPL用于检测权限 低 3 位用于选择 GDT 或者 LDT 高五位用于代表表中索引号 这个索引是21
RtlCopyMemory(&pGdt[21],&pGdt[1],sizeof(KGDTENTRY));
pGdt[21].BaseLow = (USHORT)(uNewBase&0xffff);
pGdt[21].HighWord.Bytes.BaseMid = (UCHAR)((uNewBase>>16)&0xff);
pGdt[21].HighWord.Bytes.BaseHi = (UCHAR)(uNewBase>>24); //把原来的段描述符拷过来 修改段描述符的基址
}else{
pIdtEntry[InterruptIndex].selector = 0x8;
memset(&pGdt[21],0,sizeof(KGDTENTRY));
} PageProtectOn(); u_index++;
}
} VOID HookInterruptFunc(ULONG InterruptIndex,ULONG NewInterruptFunc)
{
ULONG uNewBase; g_uOrigInterruptFunc = GetInterruptFuncAddress(InterruptIndex);
uNewBase = NewInterruptFunc - g_uOrigInterruptFunc; //段基地址 + g_uOrigInterruptFunc = NewInterruptFunc *(ULONG*)g_FilterJmp = (ULONG)NewInterrupt3OfOrigBase;
g_FilterJmp[2] = 0x8; SetInterrupt(InterruptIndex,uNewBase,TRUE);
} void UnHookInterruptFunc(ULONG InterruptIndex)
{
SetInterrupt(InterruptIndex,0,FALSE);
} VOID MyUnload(PDRIVER_OBJECT pDriverObject)
{
UnHookInterruptFunc(3);
} NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject,PUNICODE_STRING Reg_Path)
{
HookInterruptFunc(3,(ULONG)NewInterrupt3);
pDriverObject->DriverUnload = MyUnload;
return STATUS_SUCCESS;
}

最新文章

  1. linux 代码分析工具 gprof - 以wpa_supplicant为例
  2. asp.net 新项目遇到的坑
  3. SQL*Loader之CASE8
  4. InnoDB源码分析--缓冲池(二)
  5. golang学习之旅:使用go语言操作mysql数据库
  6. ActionScript 3 中的强制类型转换
  7. Dapper使用方法
  8. 使用HttpServletRequestWrapper在filter修改request参数
  9. opencv 用户文档 错误更正 仿射变换
  10. 我只能说,CDH5真的屌爆了!!!
  11. jQuery easyUI Pagination控件自定义div分页(不用datagrid)
  12. SLF4J - 一个允许你统一日志记录API的抽象层
  13. 【SSH系列】hibernate映射 -- 一对一双向关联映射
  14. 实现CString的Format功能,支持跨平台
  15. Linux新加磁盘挂载和重启自动挂载
  16. UOJ22 UR #1外星人(动态规划)
  17. 【easy】530. Minimum Absolute Difference in BST
  18. vcenter 不可访问虚拟机
  19. 【工具相关】Web-ionic-ionicLab的使用
  20. 使用SpringBoot配置了 server.servlet.path后无效的解决方案

热门文章

  1. C/C++ Qt 自定义Dialog对话框组件应用
  2. linux中为何每次修改完配置文件后都需要重新加载配置文件
  3. selenium定位元素方法汇总
  4. Dirichlet 前缀和的几种版本
  5. 一个好用的快速安装lnmp环境包lnmp1-6
  6. C#时间选择
  7. 7个连环问揭开java多线程背后的弯弯绕
  8. 【leetcode】721. Accounts Merge(账户合并)
  9. oracle 拆分字符串
  10. How exactly does Google AdWords work?