开发环境采用VS 2017,主要讲的一些配置方面的问题,
比如控制台程序和DLL程序的编译设置(兼容性设置,运行库),MFC程序编译设置。
Debug模式和Release模式
Debug通常被称为调试版本,而Release通常称为发布版本。
Debug模式和Release模式唯一的区别就是在VS开发环境里编译选项的区别。
假如在Debug模式下运行正常,代码肯定是没问题的,而在Release模式下编译通过,运行出错,是由于一些编译问题导致的,这时候就要在运行库选项里进行相应的更改。
本章主要讲的是病毒木马中一些较为常见、基础的技术。
CreateMutex()函数
HANDLE WINAPI CreateMutex(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, //指向SECURITY_ATTRIBUTES结构的指针。若此参数为NULL,则该句柄不能由子进程继承。
_In_ BOOL bInitialOwner, //此值为TRUE并创建互斥锁,则线程获得所有权
_In_opt_LPCTSTR lpName // 如果有重名对象,则会返回错误ERROR_ALREADY_EXISTS(GetLastError函数获取)。
);//该函数成功创建时,返回一个互斥对象的句柄,如果已经重复运行返回存在对象句柄。
简单来说,该函数主要用来降低暴露的风险,保证只运行一个恶意进程。通过该函数来创建一个互斥对象,来判断系统中是否重复运行着进程(对象成功创建,则被认为是首次运行,返回错误则被认为是重复执行)
可执行程序可以先加载执行,所依赖DLL在正式调用时在加载进来。
这样做可以将DLL以资源文件的形式插入到程序里,在正式调用必须DLL之前,程序都可以正常运行。
在这段时间里,再把DLL释放到本地,保证DLL正确的执行,这样程序只需要exe文件,而不需要DLL文件。
如何把DLL文件释放到本地?
FindResource()函数
HRSRC FindResource(
HMODULE hModule, //包含所需资源的模块句柄,如果是程序本身,可以置为NULL
LPCWSTR lpName, //可以是资源名称或资源ID
LPCWSTR lpType //资源类型,在这里也就是我们自己指定的资源类型
);
SizeofResource()函数
HRSRC SizeofResource(
HMODULE hModule, //模块句柄,同上
HRSRC hResInfo //需要加载的资源句柄,这里也就是FindResource的返回值
);
LoadResource()函数
HGLOBAL LoadResource(
HMODULE hModule, //模块句柄,同上
HRSRC hResInfo //需要加载的资源句柄,这里也就是FindResource的返回值
);
LockResource()函数
HGLOBAL LoadResource(
HGLOBAL hResData //指向内存中要锁定的资源数据块,这里也就是LoadResource的返回值
);
句柄,SECURITY_ATTRIBUTES,GetModuleHandle
为了方便伪装,病毒和木马需要将要执行的ShellCode或DLL注入到目标进程,其中DLL注入最为普遍。
只要其注入成功,则DLL已经成功加载到目标进程空间,简单易用。
Windows中大部分应用程序都是基于消息机制的,钩子则是用来钩住这些消息的。
HHOOK SetWindowsHookEx(
int idHook, // 钩子程序的类型
HOOKPROC lpfn, // 指向钩子程序的指针
HINSTANCE hMod, // DLL句柄
DWORD dwThreadId // 相关线程标识符,若为0,则与所有线程相关
);
如果创建的是全局钩子,那么钩子函数需要依赖于DLL,这样在对应事件发生时,DLL会被加载到事件的进程地址空间,实现DLL注入。
LRESULT WINAPI CallNextHookEx(
_In_opt_ HHOOK hhk, //保存的钩子过程,也就是SetWindowsHookEx返回值.
_In_ int nCode, //根据SetWindowsHookEx设置的钩子回调而产生的不同的nCode代码. 什么意思? 意思就是如果设置的钩子类型是鼠标消息.那么那个nCode就是鼠标消息.如果是键盘这是键盘
_In_ WPARAM wParam, //同2参数一样.附加参数. 根据钩子回调类型.附加参数有不同的意义.比如如果是鼠标.那么这个有可能代表的就是鼠标的x位置.键盘就可能是键代码
_In_ LPARAM lParam //同3参数一样.附加参数.
);
而CallNextHookEx()函数表示将当前钩子传递给钩子链的下一个钩子。
BOOL WINAPI UnhookWindowsHookEx(
_In_ HHOOK hhk //参数一是 SetWindowHookEx的返回值.也就是钩子过程句柄.
);
而当钩子不再使用时,便对钩子进行卸载。
共享内存
全局钩子的设置,回调,卸载都需要钩子句柄作为参数,将其传递给其他进程,可以采用共享内存的方法。
在DLL中创建共享内存,在DLL中创建一个变量,然后将DLL加载到多个进程空间。
只要一个修改了,其他进程DLL中的值也会改变,相当于多个进程共享内存。
远线程注入是指一个进程在另一个进程里创建线程的技术。
HANDLE OpenProcess(
DWORD dwDesiredAccess, //进程对象,会对进程的安全描述符进行检查
BOOL bInheritHandle, //若为True,由此进程创建的进程将继承该句柄
DWORD dwProcessId //本地进程PID
);
OpenProcess()用来打开本地的进程对象。
LPVOID VirtualAllocEx(
HANDLE hProcess, //进程的句柄
LPVOID lpAddress, //分配页面起始指针
SIZE_T dwSize, //分配内存大小
DWORD flAllocationType, //分配内存类型
DWORD flProtect //要分配的页面区域的内存保护
);
VirtualAllocEx()在指定的虚拟地址空间内保存、提交或者更改内存状态。
BOOL WriteProcessMemory(
HANDLE hProcess, //进程内存句柄
LPVOID lpBaseAddress, //基地址指针
LPCVOID lpBuffer, //指向缓冲区的指针
SIZE_T nSize, //写入进程字节数
SIZE_T *lpNumberOfBytesWritten //指向变量指针
);
WriteProcessMemory()在指定进程中将数据写入内存,要写入区域必须可访问。
远线程注入主要通过关键函数CreateRemoteThread()在其他进程空间中创建一个线程。
那如何在其他进程加载DLL,实现DLL注入?
首先程序在加载一个DLL的时候,会调用LoadLibrary()来实现DLL的动态加载,LoadLibrary()只能传递一个参数(要加载的DLL路径字符串)。
而CreateRemoteThread()中传递的是目标进程空间的函数地址以及多线程参数。
通过上面的分析,如果我们先获取LoadLibrary()的函数地址,然后获取进程空间内的某个DLL路径,那么就可以将函数地址与DLL路径传递给CreateRemoteThread(),在目标进程空间创建一个多线程,而这个多线程就是LoadLibrary()加载DLL。
原理我们大致清晰了,但是还有两个问题,一是如何找到LoadLibrary()的函数地址,二是如何向目标进程中写入DLL路径字符串数据。
第一个问题,Windows才用了基址随机化 ASLR 机制,导致每次开机时系统DLL的加载基址都不同,DLL导出函数的地址也不一样。但是,有一些DLL加载基地址开机之后就固定了下来,例如kernel32.dll,ntdll.dll。
也就是说,进程不同,但是开机之后,kernel32.dll的加载基址在各个进程中都相同,导出函数地址也相同,所以,自己的LoadLibrary()的函数地址和其他进程的函数地址也是相同的。
第二个问题可以由之前的函数VirtualAllocEx()在目标中申请内存,再通过WriteProcessMemory()写入目标进程。
这样,便可以通过CreateRemoteThread()来实现远线程注入。
在对一些系统服务进程进行注入的时候,上述方法并不能成功。这是用于SESSION 0隔离机制,传统的远线程注入并不能通过SESSION 0机制的,我们需要新的方法来进行远线程注入。
ZwCreateThreadEx()
DWORD WINAPI ZwCreateThreadEx(
PHANDLE ThreadHandle,
ACCESS_MASK DesireAccess,
LPCVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
ULONG CreateThreadFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaximumStackSize,
LPVOID pUnkown
); //64位系统下,函数声明
DWORD WINAPI ZwCreateThreadEx(
PHANDLE ThreadHandle,
ACCESS_MASK DesireAccess,
LPCVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
DWORD dw1,
DWORD dw2,
LPVOID pUnkown
); //32位系统下,函数声明
与传统远线程注入唯一不同的是使用了更为底层的ZwCreateThreadEx()。该函数能够突破SESSION 0。由于该函数在ntdll.dll中并没有声明,所以我们需要GetProcAddress()从ntdll.dll里获取函数导出地址。
ZwCreateThreadEx()比CreateRemoteThread()更为底层,CreateRemoteThread()是调用ZwCreateThreadEx()创建的,那为什么CreateRemoteThread()会失败呢?
在内核6.0之前,CreateRemoteThread()还是可以的。但是在内核6.0之后引入了会话隔离机制。他在创建一个进程之后并不立即运行,而是先挂起进程,在查看要运行的进程所处在的会话层,再决定是否恢复。
使用CreateRemoteThread()进行远线程注入会发现调用ZwCreateThreadEx()的第七个参数CreateSuspended/CreateThreadFlag的值为1,它会导致线程挂起后一直无法恢复。这就是失败的原因。
所以,系统服务进程想注入成功,需要直接调用ZwCreateThreadEx(),将第七个参数设置为0,这样线程创建完就会恢复运行,成功注入。
DLL中调用MessageBox弹窗来显示注入是否成功,那么由于会话隔离,弹窗在系统服务程序里不能显示。微软提供了WTS函数来实现功能
APC为异步过程调用,是一种并发机制。
DWORD QueueUserAPC(
PAPCFUNC pfnAPC, //执行函数地址
HANDLE hThread, //线程句柄
ULONG_PTR dwData //传递的参数(DLL路径)
);
每一个线程都有自己的APC队列,使用QueueUserAPC()把一个APC函数压入队列。Windows系统会发出一个软中断去执行这些APC函数,对于用户态的APC队列,当线程处在可警告状态才会执行这些APC函数。一个线程在内部使用SignalObjectAndWait()、SleepEx()等函数把自己挂起便是进入可警告状态,此时便会执行APC函数。
首先通过OpenProcess()打开目标进程,获取进程句柄;然后通过函数CreateToolhelp32Snapshot()、Thread32First()以及Thread32Next()遍历进程快照,获取目标进程的所有线程ID;紧接着调用VirtualAllocEx()在目标进程申请内存,通过WriteProcessMemory()向内存写入DLL的注入路径;最后,遍历线程ID,获取线程句柄。调用QueueUserAPC()向线程中插入APC函数,设置APC函数地址为LoadLibraryA()的地址。这样只要目标进程中任意线程被唤醒,便会执行APC,完成DLL注入。
病毒木马植入计算机之后,会启动攻击模块老对用户的计算机数据实施攻击。通常,植入和攻击使处于不同模块之中的,模块化可以减小被发现的风险。
WinExec()、ShellExecute()、CreateProcess()
UINT WINAPI WinExec(
_In_ LPCSTR lpCmdLine, //指向一个空结束的字符串,串中包含将要执行的应用程序的命令行(文件名加上可选参数)
_In_ UINT uCmdShow //定义Windows应用程序的窗口如何显示
);
HINSTANCE ShellExecute(
_In_opt_ HWND hwnd, //用于指定父窗口句柄
_In_opt_ LPCTSTR lpOperation, //用于指定要进行的操作。其中“open”操作表示执行由FileName参数指定的程序,或打开由FileName参数指定的文件或文件夹;“print”操作表示打印由FileName参数指定的文件;“explore”操作表示浏览由FileName参数指定的文件夹。当参数设为nil时,表示执行默认操作“open”。
_In_ LPCTSTR lpFile, //用于指定要打开的文件名、要执行的程序文件名或要浏览的文件夹名。
_In_opt_ LPCTSTR lpParameters, //若FileName参数是一个可执行程序,则此参数指定命令行参数,否则此参数应为nil或PChar(0)。
_In_opt_ LPCTSTR lpDirectory, //用于指定默认目录。
_In_ INT nShowCmd //若FileName参数是一个可执行程序,则此参数指定程序窗口的初始显示方式,否则此参数应设置为0。
);
BOOL CreateProcess(
_In_opt_ LPCTSTR lpApplicationName, //指向一个以空结尾的串,他指定了要执行的模块
_Inout_opt_ LPTSTR lpCommandLine, //指向一个以空结尾的串,该串定义了要执行的命令行。
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes, //指向一个SECURITY_ATTRIBUTES结构,该结构决定了返回的句柄是否可被子进程继承。
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, //指向一个SECURITY_ATTRIBUTES结构,该结构决定了返回的句柄是否可被子进程继承。
_In_ BOOL bInheritHandles, //表明新进程是否从调用进程继承句柄。
_In_ DWORD dwCreationFlags, //定义控制优先类和进程创建的附加标志。
_In_opt_ LPVOID lpEnvironment, //指向一个新进程的环境块。
_In_opt_ LPCTSTR lpCurrentDirectory, //指向一个以空结尾的串,该串定义了子进程的当前驱动器和当前目录。
_In_ LPSTARTUPINFO lpStartupInfo, //指向一个STARTUPINFO结构,该结构定义了新进程的主窗口将如何显示。
_Out_ LPPROCESS_INFORMATION lpProcessInformation //指向PROCESS_INFORMATION结构,该结构接受关于新进程的表示信息。
);
在一个进程中创建一个新进程,最简单的无异于通过 WIN32API 函数,来创建新进程,关键是对函数参数的理解,窗口显示方式也尤为重要,WinExec()和ShellExecute()通过设置 SW_HIDE 运行程序隐藏窗口,对于其他程序窗口不能成功隐藏。
而CreateProcess()可以将 STARTUPINFO 结构体中的启动标志设置为STARTF_USESHOWWINDOW 表示成员显示方式有效。然后将 wShowWindow 置为隐藏窗口(SW_HIDE)。
WinExec()使用方便,但是是个老函数,ShellExecute()可以指定运行时工作路径。WinExec()必须得到 GetMessage 或超时之后才返回,而ShellExecute()CreateProcess()都是无需等待直接返回。
前面已经介绍了 SESSION 0 隔离机制。同样的我们也可以使用微软提供的一系列 WTS 开头的函数,完成服务层与用户层的交互。
WTSGetActiveConsoleSessionId()
/*检索控制台会话的标识 Session Id 。控制台会话是当前连接到物理控制台的会话。*/
DWORD WTSGetActiveConsoleSessionId( //执行成功会返回连接到物理控制台的会话标识符
);
WTSQueryUserToken()
/*获取由 Session Id 制定的登录用户的主访问令牌。若想成功调用此功能,则应用程序必须在本地系统账户*/
/*上下文运行,并具有 SE_TCB_NAME 特权*/
BOOL WTSQueryUserToken(
ULONG SessionId, //远程桌面服务会话标识符
PHANDLE phToken //成功,则会收到一个指向登录用户令牌句柄的指针
);
DuplicateTokenEx()
/*创建一个新的访问令牌,它与现有令牌重复,此功能可以创建主令牌或模拟令牌*/
BOOL DuplicateTokenEx(
HANDLE hExistingToken, //使用 TOKEN_DUPLICATE 访问权限打开访问令牌的句柄
DWORD dwDesiredAccess, //指定新令牌的请求访问权限。要相对请求调用者的有效访问权限,请指定 MAXIMUM_ALLOWED
LPSECURITY_ATTRIBUTES lpTokenAttributes, //指向 SECURITY_ATTRIBUTES 结构的指针,该结构指定新令牌的安全描述符,并确定进程是否可以继承令牌。
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, //指定SECURITY_IMPERSONATION_LEVEL 枚举中指示新令牌模拟级别的值
TOKEN_TYPE TokenType,
PHANDLE phNewToken // 指向接收新令牌的 HANDLE 变量的指针
);
CreateEnvironmentBlock()
/*检索指定用户的环境变量,然后将此块传递给 CreateProcessAsUser 函数*/
BOOL CreateEnvironmentBlock(
LPVOID *lpEnvironment, //已接收到的新环境块的指针
HANDLE hToken, //如果这是主令牌,则令牌必须还有 TOKEN_QUERY 和 TOKEN_DUPLICATE 访问权限。若该令牌是模拟令牌,则必须具有 TOKEN_QUERY 权限
BOOL bInherit //指定是否可以继承当前进程的环境
);
CreateProcessAsUser()
/*创建一个新进程及主线程,新进程在有指定令牌表示的用户安全上下文中运行*/
BOOL CreateProcessAsUserA(
HANDLE hToken, //表示用户主令牌的句柄
LPCSTR lpApplicationName, //要执行模块名称
LPSTR lpCommandLine, //要执行命令行
LPSECURITY_ATTRIBUTES lpProcessAttributes, //指向 SECURITY_ATTRIBUTES 结构的指针(新进程)
LPSECURITY_ATTRIBUTES lpThreadAttributes, //指向 SECURITY_ATTRIBUTES 结构的指针(新线程)
BOOL bInheritHandles, //若为 TRUE ,则调用进程中的每个可继承句柄都由新进程继承
DWORD dwCreationFlags, //控制优先级和进程创建的标志
LPVOID lpEnvironment, //指向新进程环境块的指针
LPCSTR lpCurrentDirectory, //指向进程当前目录的完整路径
LPSTARTUPINFOA lpStartupInfo, //指向 STARTUPINFO 或 STARTUPINFOEX 结构的指针
LPPROCESS_INFORMATION lpProcessInformation //指向 PROCESS_INFORMATION 结构的指针
);
由于 SESSION 0 的隔离,系统服务进程内不能直接调用CreateProcess()来创建进程,只能通过CreateProcessAsUser()来创建。这样,创建的进程才会显示UI界面,与用户进行交互。
在 SESSION 0 中创建桌面进程的实现流程如下。
首先,调用WTSGetActiveConsoleSessionId()来获取当前程序的会话ID,即 Session Id。根据 Session Id 继续调用WTSQueryUserToken()来检索用户令牌,并获取对应的用户令牌句柄。
其次,使用DuplicateTokenEx()创建一个新令牌,复制上面获取的用户令牌。设置新令牌的访问权限为 MAXIMUM_ALLOWED。新访问令牌的模拟级别为 Security_Identification,且令牌类型为 Token_Primary。
最后根据新令牌调用CreateEnvironBlock()创建一个环境块,传递给CreateProcessAsUser()使用,获取环境块之后,就可以调用CreateProcessAsUser()来创建用户桌面进程。
很多木马病毒都具有模拟 PE 加载器的功能,他们把 DLL 或者 exe 等 PE 文件从内存中直接加载到木马病毒的内存中执行。
如果程序需要动态调用 DLL 文件内存加载运行技术可以把这些 DLL 作为资源插入自己的程序中,直接在内存中加载运行即可。
首先就是要把DLL文件按照映像对齐大小映射到内存中,不可直接将DLL文件数据存储到内存中。PE文件有两个对齐字段,一个是映像对齐大小SectionAlignment,一个是文件对齐大小FileAlignment。其中,映像对齐大小是PE文件加载到内存中所用的对齐大小,而文件对齐大小是PE文件存储在本地磁盘所用的对齐大小。一般文件对齐大小会比映像对齐大小要小,这样文件会变小,以此节省磁盘空间。
然而,成功映射内存数据之后,在 DLL 程序中会存在硬编码数据,硬编码都是以默认的加载基址作为基址来计算的。由于 DLL 可以任意加载到其他进程空间中,所以 DL L的加载基址并非固定不变。当改变加载基址的时候,硬编码也要随之改变,这样 DLL 程序才会计算正确。但是,如何才能知道需要修改哪些硬编码呢?换句话说,如何知道硬编码的位置?答案就藏在PE结构的重定位表中,重定位表记录的就是程序中所有需要修改的硬编码的相对偏移位置。
根据重定位表修改硬编码数据后,这只是完成了一半的工作。DLL作为一个程序,自然也会调用其他库函数,例如MessageBox。那么DLL如何知道MessageBox函数的地址呢?它只有获取正确的调用函数地址后,方可正确调用函数。PE结构使用导入表来记录PE程序中所有引用的函数及其函数地址。在DLL映射到内存之后,需要根据导入表中的导入模块和函数名称来获取调用函数的地址。若想从导入模块中获取导出函数的地址,最简单的方式是通过GetProcAddress函数来获取。但是为了避免调用敏感的WIN32 API函数而被杀软拦截检测,本书采用直接遍历PE结构导出表的方式来获取导出函数地址,这要求读者熟悉导出表的具体操作原理。
完成上述操作之后,DLL加载工作才算完成,接下来便是获取入口地址并跳转执行以便完成启动。
具体的实现流程总结如下。
首先,在DLL文件中,根据PE结构获取其加载映像的大小SizeOfImage,并根据SizeOfImage在自己的程序中申请可读、可写、可执行的内存,那么这块内存的首地址就是DLL的加载基址。
其次,根据DLL中的PE结构获取其映像对齐大小SectionAlignment,然后把DLL文件数据按照SectionAlignment复制到上述申请的可读、可写、可执行的内存中。
接下来,根据PE结构的重定位表,重新对重定位表进行修正。
然后,根据PE结构的导入表,加载所需的DLL,并获取导入函数的地址并写入导入表中。
接着,修改DLL的加载基址ImageBase。
最后,根据PE结构获取DLL的入口地址,然后构造并调用 DllMain函数,实现DLL加载。
而exe文件相对于DLL文件实现原理唯一的区别就在于构造入口函数的差别,exe 不需要构造DllMain函数,而是根据PE结构获取exe的入口地址偏移 AddressOfEntryPoint 并计算出入口地址,然后直接跳转到入口地址处执行即可。
要特别注意的是,对于exe文件来说,重定位表不是必需的,即使没有重定位表,exe也可正常运行。因为对于exe进程来说,进程最早加载的模块是exe模块,所以它可以按照默认的加载基址加载到内存。对于那些没有重定位表的程序,只能把它加载到默认的加载基址上。如果默认加载基址已被占用,则直接内存加载运行会失败。
PE文件
对于一个木马病毒来说,不仅是如何破坏,还要如何执行。
解决永久驻留的问题的第一步便是如何随系统的启动而启动
为了方便用户,各种程序都会提供开机自启动功能,这样可以伴随系统启动自己运行起来,以修改注册表的方法最为广泛。
RegOpenKeyEx()
/*打开一个指定的注册表键*/
LSTATUS RegOpenKeyExA(
HKEY hKey, //打开或者预定义键(HKEY_CLASSES_ROOT HKEY_CURRENT_USER HKEY_LOCAL_MACHINE HKEY_USERS HKEY_CURRENT_CONFIG)
LPCSTR lpSubKey, //指向一个非中断字符串将要打开键的名称
DWORD ulOptions, //保留 为0
REGSAM samDesired, //对指定键希望得到的访问权限进行访问标记
PHKEY phkResult //指向一个打开注册表键的句柄的指针
);
RegSetValueEx()
/*在注册表项设置指定值的数据和类型*/
LSTATUS RegSetValueExA(
HKEY hKey, //指定一个已打开项的句柄,或一个标准项名
LPCSTR lpValueName, //指向一个字符串的指针,该字符串包含欲设置值的名称
DWORD Reserved, //保留值 强制为0
DWORD dwType, //指定储存的数据类型
const BYTE *lpData, //指向一个缓冲区,它包含了为指定值名称存储的数据
DWORD cbData //指定由 lpData 参数所指向的数据大小
);
Windows 提供了专门的开机自启动注册表。每次开机完成后,他都会在这个注册表键下遍历键值,以获取键值中的程序路径,并创建进程启动程序。
修改注册表的方式主要区别在于 注册表键路径 ,我们使用两个路径分别是 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run 和 HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run
它们的区别仅是主键不同,一个是 HKEY_CURRENT_USER ,另一个是 HKEY_LOCAL_MACHINE。但是它们功能类似,都可以实现开机自启动。
需要注意的是,修改 HKEY_LOCAL_MACHINE 主键的注册表,需要程序的管理员权限,而修改HKEY_CURRENT_USER 只需要用户默认权限。
在64位系统上,需要注意注册表重定向的问题。在64位 Windows 系统中,为了兼容32位程序的正常运行,64位 Windows 系统采用重定向机制。系统为关键文件夹和关键注册表创建了两个副本,使得32位程序在64位 Windows 系统上不仅能操作关键文件夹和关键注册表,还可以避免和64位程序冲突。
SHGetSpecialFolderPath()
/*获取指定的系统路径*/
BOOL SHGetSpecialFolderPath(
HWND hwndOwner, //窗口所有者句柄
LPTSTR lpszPath, //返回路径的缓冲区
int nFolder, //系统路径的CSIDL标识
BOOL fCreate //指示文件夹不存在时是否要创建
);
Windows 系统中自带快速启动文件夹,只要把文件放入其中,系统在启动时就会自动地加载相应的程序,实现开机自启动。
快速启动目录并不是一个固定目录,每台计算机的快速启动目录都不相同。但是程序可以通过函数SHGetSpecialFolderPath()来获取 Windows 快速启动目录的路径,快速启动目录的 CSIDL标识为 CSIDL_STARTUP。
Windows 可以通过设置计划任务来执行一些定时任务,而我们希望在用户登录时出发,执行启动指定路径程序的操作,从而实现开机自启动。
使用Windows Shell实现计划任务,会涉及 COM 组件 接口的调用。
整个方法可以分为以下几个部分:
初始化操作
首先,通过 CoIntialize 函数初始化 COM 接口环境。
然后,调用 CoCreateInstance 函数创建任务服务对象 ITaskService,将其连接到任务服务上。
最后,从 ITaskService 对象中获取根任务 Root Task Folder 的指针对象 **ITaskFolder,这个指针指向的是新注册的任务。
创建任务计划操作
首先, 从 ITaskService 对象中创建一个任务定义对象 ITaskDefinition,它用来创建任务。
然后,对任务对象 ITaskDefinition 进行设置
最后,使用 ITaskFolder 对象并利用任务定义对象 ITaskDefinition 的设置,注册任务计划。
删除计划任务操作
ITaskFolder 对象存储着已经注册成功的任务计划信息,程序只需要调用 DeleteTask 函数,并将任务计划的名称传入,就可以删除指定名称的计划任务了
系统服务大多数都是随着系统启动而启动的。
系统服务运行在 SESSION 0 ,由于系统服务的 SESSION 0隔离,阻断了系统服务和用户桌面进程之间进行交互和通信的桥梁。各个会话之间相互独立,在不同会话中运行的实体,相互之间不能发送 Windows 消息、共享UI元素,或是在没有指定有权限访问全局名字空间的情况下共享核心对象。
OpenSCManager()
/*建立一个到服务控制管理器的连接,并开始指定的数据库*/
SC_HANDLE OpenSCManagerA(
LPCSTR lpMachineName, //指定目标计算机的名称
LPCSTR lpDatabaseName, //指定将要打开的服务控制管理数据库的名称
DWORD dwDesiredAccess //指定服务访问控制管理器的权限
);
CreateService()
/*建立一个到服务控制管理器的连接,并开始指定的数据库*/
SC_HANDLE WINAPI CreateService(
_In_ SC_HANDLE hSCManager, //指向服务控制管理其数据库的句柄
_In_ LPCTSTR lpServiceName, //要安装服务的名称
_In_opt_ LPCTSTR lpDisplayName, //用户界面程序用来识别服务的显示名称
_In_ DWORD dwDesiredAccess, //对服务的访问
_In_ DWORD dwServiceType, //访问类型
_In_ DWORD dwStartType, //服务启动选项
_In_ DWORD dwErrorControl, //产生错误的保护措施
_In_opt_ LPCTSTR lpBinaryPathName, //服务程序的二进制文件
_In_opt_ LPCTSTR lpLoadOrderGroup, //指向在排序组的名称
_Out_opt_ LPDWORD lpdwTagId, //接受由lpLoadOrderGroup参数指定的组中唯一标记值变量
_In_opt_ LPCTSTR lpDependencies, //空分割名称的服务或加载顺序组程序在这个服务开始之前的双空终止数组的指针
_In_opt_ LPCTSTR lpServiceStartName, //该服务应运行的账户名称
_In_opt_ LPCTSTR lpPassword //由lpServiceStartName参数指定的账户名密码
);
OpenService()
/*建立一个到服务控制管理器的连接,并开始指定的数据库*/
SC_HANDLE OpenServiceA(
SC_HANDLE hSCManager, //指向SCM数据库的句柄
LPCSTR lpServiceName, //要打开的服务的名字
DWORD dwDesiredAccess //指定服务权限
);
StartService()
/*启动服务*/
SC_HANDLE OpenService(
SC_HANDLE hService, //由CreateService或OpenService返回的服务句柄
DWORD dwNumServiceArgs, //下一个形参lpServiceArgVectors的字符串个数
LPCSTR *lpServiceArgVectors, //传递给服务ServiceMain的参数
);
创建自启动系统服务进程将分成一下两部分:
创建启动系统服务
首先通过OpenSCManager()函数打开服务控制管理器数据库并获取数据库的句柄。
若要创建服务,则调用函数CreateService()开始创建服务,并指明创建的服务类型是 SERVICE_WIN32_OWN_PROCESS 系统服务,同时设置 SERVICE_AUTO_START 为开机自启动。
接着,根据服务句柄考虑以下操作,若是启动,则使用StartService();若是停止,则使用ControlService();若是删除DeleteService()。
最后,关闭服务句柄和服务控制管理器数据库句柄。
系统服务程序
自启动服务程序要求程序创建服务入口点函数。
调用StartServiceCtrlDispatcher()将程序主线程连接到服务控制管理程序。服务控制管理程序启动服务程序后,等待服务程序主函数调用StartServiceCtrlDispatcher()。
执行服务初始化任务,首先调用RegisterServiceCtrlHandle()定义控制处理程序函数,初始化后通过SetServiceStatus()设定运行状态,然后运行服务代码。
访问令牌
如果把权限看做门禁卡,那么计算机便是一座有诸多门禁的大楼。没有足够的权限,也不能窃取或者修改计算机中关键的数据,因此提权技术尤其关键。
自从 VISTA 系统引入 UAC 后,涉及权限的操作都会有弹窗提示,所以VISTA 之后的系统主要是针对 UAC 不弹窗静默提权,即 BypassUAC。
OpenProcessToken()
/*打开与进程关联的访问令牌*/
BOOL OpenProcessToken(
HANDLE ProcessHandle, //打开访问令牌的进程句柄
DWORD DesiredAccess, //指定一个访问掩码
PHANDLE TokenHandle //指向一个句柄的指针
);
LookupPrivilegeValue()
/*查看系统权限的特权值,返回信息到一个LUID结构体里*/
BOOL LookupPrivilegeValueA(
LPCSTR lpSystemName, //指向一个字符串指针,该字符串指向要获取特权值的系统名称
LPCSTR lpName, //指向空终止字符串的指针,并制定特权的名称
PLUID lpLuid //指向LUID变量的指针
);
AdjustTokenPrivileges()
/*启用或禁止指定访问令牌中的权限*/
BOOL AdjustTokenPrivileges(
HANDLE TokenHandle, //指向访问令牌的句柄
BOOL DisableAllPrivileges, //指定该功能是否禁用所有令牌的权限
PTOKEN_PRIVILEGES NewState, //指向TOEKN_PRIVILEGES结构的指针
DWORD BufferLength, //指定由PreviousState参数指向的缓冲区大小
PTOKEN_PRIVILEGES PreviousState, //指向缓冲区的指针
PDWORD ReturnLength //指向一个变量的指针
);
要提升访问令牌权限,首先要获取进程的访问令牌,然后将访问令牌权限修改为指定权限。但是系统内部并不直接识别权限名称,而是识别 LUID 值,之后传递给系统,完成修改。
首先,程序需要调用OpenProcessToken()打开进程令牌,并获取 TOKEN_ADJUST_PRIVILEGE 的令牌句柄。
接着,调用LookupPrivilegeValue(),获取本地系统指定特权名称 LUID ,这个 LUID 值相当于特权的身份证号。
然后,开始对进程令牌特权结构体 TOEKN_PRIVILEGES 进行赋值,设置新特权数量、特权对应的 LUID 值以及特权的属性状态。
最后,程序调用AdjustTokenPrivileges()对进程令牌的特权进行修改,将之前设置好的新特权设置到进程令牌之中。
参考1,