命令提示符也就是命令行控制台,新版本也叫做Windows 终端。如何做到当命令被输入控制台窗口后能够做到过滤呢?
其中,有一种就是键盘钩子判断键盘输入,但实用性可能不高。
另外一种方法就是获取控制台缓冲区的文本,了解用户接下来要使用那种指令,然后对其进行相应的过滤。
上面的图片展示了本节我们要实现的命令拦截效果。我们甚至只需要过滤具有特征性的参数调用。
首先我们需要明白的是,无论是新版还是旧版控制台(兼容性)他的控制台缓冲区数据都是在一个名为cmd.exe的进程中完成的。
尽管新版本引入了UWP界面的外壳组件:OpenConsole.exe、WindowsTerminal.exe,他们基本上处理图形界面(GUI)。
微软提供了很多接口用于控制台的高级应用。
这里我们需要谈的API主要有两个:
ReadConsole
从控制台输入缓冲区读取字符输入,并将其从缓冲区中删除。
这是一个很好的函数,它就像是一个内部的钩子一样,在用户将数据输入时候阻断命令的执行,并且将文本汇报给调用方。只不过这个函数只对当前序列的所有控制台线程有效,对远线程无效。
我们来看看它的原型:
BOOL WINAPI ReadConsole(
_In_ HANDLE hConsoleInput,
_Out_ LPVOID lpBuffer,
_In_ DWORD nNumberOfCharsToRead,
_Out_ LPDWORD lpNumberOfCharsRead,
_In_opt_ LPVOID pInputControl
);
hConsoleInput [in]
控制台输入缓冲区的句柄。 该句柄必须具有 GENERIC_READ 访问权限。 有关详细信息,请参阅控制台缓冲区安全性和访问权限。
lpBuffer [out]
指向接收从控制台输入缓冲区读取数据的缓冲区的指针。
nNumberOfCharsToRead [in]
要读取的字符数。 lpBuffer 参数指向的缓冲区的大小应至少nNumberOfCharsToRead * sizeof(TCHAR)
为字节。
lpNumberOfCharsRead [out]
指向接收实际读取字符数的变量的指针。
pInputControl [in, 可选]
指向 CONSOLE_READCONSOLE_CONTROL 结构的指针,该结构指定要向读取操作末尾发出信号的控制字符。 此参数可以为 NULL。
此参数默认需要 Unicode 输入。 对于 ANSI 模式,请将此参数设置为 NULL。
为了防止读取时缓冲区不足,一般初始化一个1024*2字节的缓冲区用于存放文本。
你可以这样调用:
setlocale(LC_ALL, "");// 使字符串中文支持
HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);// 获取控制台输入缓冲区的句柄
TCHAR cBuf[1024]{};// 定义一个字符串变量指针
DWORD ret = 0; // 用于存储实际读取到的文本长度
RtlZeroMemory(cBuf, sizeof(cBuf) / sizeof(TCHAR));// 初始化
ReadConsoleW(hStdIn, &cBuf, ARRAYSIZE(cBuf), &ret, NULL);// 拦截缓冲区输入
会读取也要会写:
WriteConsole
从当前光标位置开始,将字符串写入控制台屏幕缓冲区。
这个函数主要用于打印文本,类似于printf(" ")
函数原型:
BOOL WINAPI WriteConsole(
_In_ HANDLE hConsoleOutput,
_In_ const VOID *lpBuffer,
_In_ DWORD nNumberOfCharsToWrite,
_Out_opt_ LPDWORD lpNumberOfCharsWritten,
_Reserved_ LPVOID lpReserved
);
你可以这样调用:
HANDLE hStdout;
DWORD dwChars;
TCHAR buf[] = _T("您好,世界,Helloworld");
int len = lstrlen(buf);
hStdout = GetStdHandle(STD_OUTPUT_HANDLE);// 获取输出句柄
if (hStdout==INVALID_HANDLE_VALUE)
{
return -1;
}
WriteConsole(hStdout,buf,len,&dwChars,NULL);
然后就是一个很简单的一点,这个函数用于新版本输入文本的复原是无效的,如果要对拦截下来的命令进行恢复,我的方案是用管道执行命令然后返回执行结果,这个过程是同步执行的。
这段代码可以用于实现一个简单的执行管道:替代system()函数,
/// 参数一传入指令用法和system()函数相同,参数二返回最后执行结果的最后一行文本
/// 参数三返回参数二的长度
/// 执行过程的每一行都会实时打印
int _System(const char* cmd, char* pRetMsg, int msg_len)
{
FILE* fp;
char* p = NULL;
int res = -1;
if (cmd == NULL || pRetMsg == NULL || msg_len < 0)
{
printf("Param Error!\n");
return -1;
}
if ((fp = _popen(cmd, "r")) == NULL)
{
printf("Popen Error!\n");
return -2;
}
else
{
memset(pRetMsg, 0, msg_len);
//get lastest result
while (fgets(pRetMsg, msg_len, fp) != NULL)
{
printf("PippeMsg: %s", pRetMsg); //print all info
}
if ((res = _pclose(fp)) == -1)
{
printf("close popenerror!\n");
return -3;
}
pRetMsg[strlen(pRetMsg) - 1] = '\0';
return 0;
}
}
不过还有一点需要注意的是,控制台窗口默认是有工作目录标志的C:\Users\UserName>。
每次管道执行完毕,新的标志符可能显示不出来,这时候需要发送回车键消息产生一个新的标识符。否则拦截将会无效(同一个标识符下只能拦截一次,这个小BUG不知道有没有更好的解决方法,望各位指点)。
然后就是解决发送回车消息的方法了。
这里使用SendInput函数向键盘队列插入新的按键消息。
SendInput 函数
合成击键、鼠标动作和按钮单击。
UINT SendInput(
[in] UINT cInputs,
[in] LPINPUT pInputs,
[in] int cbSize
);
[in] cInputs
pInputs 数组中的结构数。
[in] pInputs
输入结构的数组。每个结构表示要插入到键盘或鼠标输入流中的事件。
[in] cbSize
输入结构的大小(以字节为单位)。如果 cbSize 不是 INPUT 结构的大小,则该函数将失败。
实现:
void SendEnter()
{
INPUT inputs[2] = {};
ZeroMemory(inputs, sizeof(inputs));
inputs[0].type = INPUT_KEYBOARD;
inputs[0].ki.wVk = VK_RETURN;
inputs[1].type = INPUT_KEYBOARD;
inputs[1].ki.wVk = VK_RETURN;
inputs[1].ki.dwFlags = KEYEVENTF_KEYUP;
UINT uSent = SendInput(ARRAYSIZE(inputs), inputs, sizeof(INPUT));
if (uSent != ARRAYSIZE(inputs))
{
//OutputString(L"SendInput failed: 0x%x\n", HRESULT_FROM_WIN32(GetLastError()));
SendEnter();
}
}
然而这时候我们需要切换窗口,不然回车键就有可能在其它应用中完成:
HWND hForeWnd = GetForegroundWindow();
DWORD dwForeID = GetWindowThreadProcessId(hForeWnd, NULL);
DWORD dwCurID = GetCurrentThreadId();
AttachThreadInput(dwCurID, dwForeID, TRUE);
ShowWindow(hwnd, SW_SHOWNORMAL);
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
SetForegroundWindow(hwnd);
Sleep(50);
SetFocus(hwnd);// 设置键盘输入点
Sleep(10);
SendEnter();//模拟回车键
Sleep(50);
// 复原
SetForegroundWindow(hForeWnd);
Sleep(50);
SetFocus(hForeWnd);// 设置键盘输入点
AttachThreadInput(dwCurID, dwForeID, FALSE);
这些函数操作都需要在要注入的进程中完成,不能在调用方完成。
一个简单的注入例子如下:(编译为控制台应用)
#include <iostream>
#include <windows.h>
#include <Tlhelp32.h>
#include <stdio.h>
using namespace std;
/// <summary>
/// 根据进程名称获取进程信息
/// </summary>
/// <param name="info"></param>
/// <param name="processName"></param>
/// <returns></returns>
///
BOOL getProcess32Info(PROCESSENTRY32* info, const TCHAR processName[])
{
HANDLE handle; //定义CreateToolhelp32Snapshot系统快照句柄
handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);//获得系统快照句柄
//PROCESSENTRY32 结构的 dwSize 成员设置成 sizeof(PROCESSENTRY32)
info->dwSize = sizeof(PROCESSENTRY32);
//调用一次 Process32First 函数,从快照中获取进程列表
Process32First(handle, info);
//重复调用 Process32Next,直到函数返回 FALSE 为止
while (Process32Next(handle, info) != FALSE)
{
if (wcscmp(processName, info->szExeFile) == 0)
{
return TRUE;
}
}
return FALSE;
}
BOOL ZwCreateThreadExInjectDll(DWORD dwProcessId, const wchar_t* pszDllFileName)
{
int pathSize = (wcslen(pszDllFileName) + 1) * sizeof(wchar_t);
// 1.打开目标进程
HANDLE hProcess = OpenProcess(
PROCESS_ALL_ACCESS, // 打开权限
FALSE, // 是否继承
dwProcessId); // 进程PID
if (NULL == hProcess)
{
cout << L"打开目标进程失败!" << endl;
return FALSE;
}
// 2.在目标进程中申请空间
LPVOID lpPathAddr = VirtualAllocEx(
hProcess, // 目标进程句柄
0, // 指定申请地址
pathSize, // 申请空间大小
MEM_RESERVE | MEM_COMMIT, // 内存的状态
PAGE_READWRITE); // 内存属性
if (NULL == lpPathAddr)
{
cout << L"在目标进程中申请空间失败!" << endl;
CloseHandle(hProcess);
return FALSE;
}
// 3.在目标进程中写入Dll路径
if (FALSE == WriteProcessMemory(
hProcess, // 目标进程句柄
lpPathAddr, // 目标进程地址
pszDllFileName, // 写入的缓冲区
pathSize, // 缓冲区大小
NULL)) // 实际写入大小
{
cout << L"目标进程中写入Dll路径失败!" << endl;
CloseHandle(hProcess);
return FALSE;
}
//4.加载ntdll.dll
HMODULE hNtdll = LoadLibraryW(L"ntdll.dll");
if (NULL == hNtdll)
{
cout << L"加载ntdll.dll失败!" << endl;
CloseHandle(hProcess);
return FALSE;
}
//5.获取LoadLibraryA的函数地址
//FARPROC可以自适应32位与64位
FARPROC pFuncProcAddr = GetProcAddress(GetModuleHandle(L"Kernel32.dll"),
"LoadLibraryW");
if (NULL == pFuncProcAddr)
{
cout << L"获取LoadLibrary函数地址失败!" << endl;
CloseHandle(hProcess);
return FALSE;
}
//6.获取ZwCreateThreadEx函数地址,该函数在32位与64位下原型不同
//_WIN64用来判断编译环境 ,_WIN32用来判断是否是Windows系统
#ifdef _WIN64
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
ULONG CreateThreadFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaximumStackSize,
LPVOID pUnkown
);
#else
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
DWORD dw1,
DWORD dw2,
LPVOID pUnkown
);
#endif
typedef_ZwCreateThreadEx ZwCreateThreadEx =
(typedef_ZwCreateThreadEx)GetProcAddress(hNtdll, "ZwCreateThreadEx");
if (NULL == ZwCreateThreadEx)
{
cout << L"获取ZwCreateThreadEx函数地址失败!" << endl;
CloseHandle(hProcess);
return FALSE;
}
//7.在目标进程中创建远线程
HANDLE hRemoteThread = NULL;
DWORD dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL,
hProcess,
(LPTHREAD_START_ROUTINE)pFuncProcAddr, lpPathAddr, 0, 0, 0, 0, NULL);
if (NULL == hRemoteThread)
{
cout << L"目标进程中创建线程失败!" << endl;
CloseHandle(hProcess);
return FALSE;
}
// 8.等待线程结束
WaitForSingleObject(hRemoteThread, -1);
// 9.清理环境
VirtualFreeEx(hProcess, lpPathAddr, 0, MEM_RELEASE);
CloseHandle(hRemoteThread);
CloseHandle(hProcess);
FreeLibrary(hNtdll);
return TRUE;
}
// 要注入dll的完整路径
const wchar_t dllPath[] = L"D:\\repos\\hcpCmdIde.dll";
BOOL GetAITime(HANDLE hProcess) {
FILETIME loadStartTime, exitTime, kernelTime, userTime;
// 获取进程时间信息
::GetProcessTimes(hProcess, &loadStartTime, &exitTime, &kernelTime, &userTime);
SYSTEMTIME stCreation{};
SYSTEMTIME lstCreation{};
// 定义为中国北京时区的信息
TIME_ZONE_INFORMATION DEFAULT_TIME_ZONE_INFORMATION = { -480 };
// 文件记录时间转换为系统时间
::FileTimeToSystemTime(&loadStartTime, &stCreation);
// 系统时间转换为中国北京时区时间
SystemTimeToTzSpecificLocalTime(&DEFAULT_TIME_ZONE_INFORMATION, &stCreation, &lstCreation);
// 打印具体时间
printf("StartTime:\t %u-%u-%u %02d:%02d:%02d\n",
lstCreation.wYear,
lstCreation.wMonth,
lstCreation.wDay,
lstCreation.wHour,
lstCreation.wMinute,
lstCreation.wSecond);
return TRUE;
}
int main()
{
SetConsoleTitleW(L"TASDPI");// 设置标题
PROCESSENTRY32 info;
// 查找指定进程名的PID
if (getProcess32Info(&info, L"cmd.exe"))// 要注入cmd进程
{
HANDLE hCmd = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, info.th32ProcessID);
printf("Hook PTD[%d]\n", info.th32ProcessID);
GetAITime(hCmd);// 获取指定cmd进程启动的时刻
// 进程标识符PID。
ZwCreateThreadExInjectDll(info.th32ProcessID, dllPath);
cout << "注入成功" << endl;
}
else {
cout << "注入失败" << endl;
}
cin.get();
return 0;
}
然后就是DLL的代码:
#include "pch.h"
#include <cstdio>
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <Psapi.h>
#include <string.h>
#include <TlHelp32.h>
#include <Shlwapi.h>
#include <clocale>
#include <regex>
using namespace std;
#pragma comment(lib,"Shlwapi.lib")
#pragma comment ( lib, "Psapi.lib" )
//共享代码段
#pragma data_seg("SHARED")
int ConMode = 0;// 进程模式
#pragma data_seg()
#pragma comment(linker, "/section:SHARED,RWS")
void SendEnter()
{
INPUT inputs[2] = {};
ZeroMemory(inputs, sizeof(inputs));
inputs[0].type = INPUT_KEYBOARD;
inputs[0].ki.wVk = VK_RETURN;
inputs[1].type = INPUT_KEYBOARD;
inputs[1].ki.wVk = VK_RETURN;
inputs[1].ki.dwFlags = KEYEVENTF_KEYUP;
UINT uSent = SendInput(ARRAYSIZE(inputs), inputs, sizeof(INPUT));
if (uSent != ARRAYSIZE(inputs))
{
//OutputString(L"SendInput failed: 0x%x\n", HRESULT_FROM_WIN32(GetLastError()));
SendEnter();
}
}
string ws2s(const wstring& ws)
{
setlocale(LC_ALL, "chs");
const wchar_t* wcs = ws.c_str();
size_t dByteNum = sizeof(wchar_t) * ws.size() + 1;
//cout << "ws.size():" << ws.size() << endl;
char* dest = new char[dByteNum];
wcstombs_s(NULL, dest, dByteNum, wcs, _TRUNCATE);
string result = dest;
delete[] dest;
return result;
}
int _System(const char* cmd, char* pRetMsg, int msg_len)
{
FILE* fp;
char* p = NULL;
int res = -1;
if (cmd == NULL || pRetMsg == NULL || msg_len < 0)
{
printf("Param Error!\n");
return -1;
}
if ((fp = _popen(cmd, "r")) == NULL)
{
printf("Popen Error!\n");
return -2;
}
else
{
memset(pRetMsg, 0, msg_len);
//get lastest result
while (fgets(pRetMsg, msg_len, fp) != NULL)
{
printf("%s", pRetMsg); // print all info
}
if ((res = _pclose(fp)) == -1)
{
printf("close popenerror!\n");
return -3;
}
pRetMsg[strlen(pRetMsg) - 1] = '\0';
return 0;
}
}
BOOL SwitchToHandleCommandProcessMainWindow(HWND hwnd)
{
if (!ConMode) {
printf("GetConModeError");
return FALSE;
}
if (hwnd != NULL) {
//printf("HWND %08p\n", hwnd);
HWND hForeWnd = ::GetForegroundWindow();
DWORD dwForeID = ::GetWindowThreadProcessId(hForeWnd, NULL);
DWORD dwCurID = ::GetCurrentThreadId();
::AttachThreadInput(dwCurID, dwForeID, TRUE);
::ShowWindow(hwnd, SW_SHOWNORMAL);
::SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
::SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
::SetForegroundWindow(hwnd);
Sleep(50);
::SetFocus(hwnd);// 设置键盘输入点
Sleep(10);
SendEnter();//模拟回车键
Sleep(50);
// 复原
::SetForegroundWindow(hForeWnd);
Sleep(50);
::SetFocus(hForeWnd);// 设置键盘输入点
::AttachThreadInput(dwCurID, dwForeID, FALSE);
}
else
return FALSE;
//ShowWindow(lmshwnd, SW_MAXIMIZE);
return TRUE;
}
BOOL SetObjectCommandHook() {
constexpr int length = 15;
const auto characters = TEXT("abcdefghi9182345jklmnopqrstuv211935960473wxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890");
TCHAR nwsName[length + 1]{};
TCHAR OleName[MAX_PATH + 1]{};
srand((int)time(0));// 根据系统时间生成随机种子
for (int j = 0; j != length; j++)
{
nwsName[j] += characters[rand() % 80];
}
GetConsoleTitleW(OleName, MAX_PATH);// 备份
Sleep(100);
SetConsoleTitleW(nwsName);// 重置窗口标题
Sleep(300);
HWND hwnds = FindWindow(L"CASCADIA_HOSTING_WINDOW_CLASS", nwsName);
if (hwnds == NULL) {
hwnds = FindWindow(L"ConsoleWindowClass", nwsName);
ConMode = 2;
}
else {
ConMode = 1;
}
SwitchToHandleCommandProcessMainWindow(hwnds);// 使得控制台缓冲区输入Hook立即生效
SetConsoleTitleW(OleName);// 重置窗口标题
wprintf(L"@RioCth Hook Command! Happy!");
SendEnter();//模拟回车键
return TRUE;
}
void InjectProc(void) {
setlocale(LC_ALL, "");
HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);
TCHAR cBuf[1024]{};
while (TRUE) {
/* Display Options */
DWORD ret = 0;
do
{
RtlZeroMemory(cBuf, sizeof(cBuf) / sizeof(TCHAR));
ReadConsoleW(hStdIn, &cBuf, ARRAYSIZE(cBuf), &ret, NULL);
// 根据参数的关键特征进行判断,从而拦截
if (StrStrIW(cBuf, L"/IM") || StrStrIW(cBuf, L"/PID") || StrStrIW(cBuf, L"/F") || StrStrIW(cBuf, L"/T")) {
if (StrStrIW(cBuf, L"explorer")) {
time_t nowtime;
struct tm p[40]{};
time(&nowtime);
localtime_s(p, &nowtime);
wprintf(L"[Time\t%d-%d-%d:%02d:%02d:%02d]\nCommandLine: %s 操作未能完成,原因:拒绝访问。",
1900 + p->tm_year, 1 + p->tm_mon, p->tm_mday,
p->tm_hour, p->tm_min, p->tm_sec, cBuf);
SendEnter();//模拟回车键
}else if (0 != _wcsicmp(cBuf, L"\n")) {
cBuf[wcslen(cBuf) - 1] = L'\0';
char PipeResult[256]{};
// 用管道执行原始命令
ret = _System(ws2s(cBuf).c_str(), PipeResult, sizeof(PipeResult));
SendEnter();//后面不能使用Sleep否则有BUG
}
}
else if(0 != _wcsicmp(cBuf, L"\n")) {
cBuf[wcslen(cBuf) - 1] = L'\0';
char PipeResult[256]{};
// 用管道执行原始命令
ret = _System(ws2s(cBuf).c_str(), PipeResult, sizeof(PipeResult));
SendEnter();
}
} while (0 != wcscmp(cBuf, L"\0"));//后面不能使用Sleep否则有BUG
}
return ;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
HANDLE hThread = NULL;
DWORD threadID1 = 0;
int Counter = 0;
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)InjectProc, NULL, 0, &threadID1);
while (!hThread && Counter < 5) {
hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)InjectProc, NULL, 0, &threadID1);
Counter++;
}
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
以上就是通过Dll注入实现屏蔽控制台某种指令的方法。
转载请注明出处:@涟幽516
更新时间:2023.1.29