深入剖析USB_Dumper原理
软件下载地址:sourceforge
软件运行界面:
点击 Start 就弹出一个窗口提示 “USB Dumper have started !!! Close it Manually from the Task Manager !!!”,并开始在后台进行监听,可以通过任务管理器杀掉。当 U 盘插入时,就会自动将盘上的数据拷贝到 C:\USB
目录下(没有的话会自行创建)
peid 查了下发现是 .NET 框架的程序,反编译工具一开始选用的 dnSpy,后来发现在局部变量的处理上没有 Reflector 好,所以换成了后者,可惜的是不能加注释,也不能重命名方法和变量……
查找关键函数
在 USB_Dumper 的名称空间下看到一个 Main 类,点开又找到两个方法 Button1_Click
和 Button2_Click
,
猜测这两个方法对应软件主界面的 Start 和 Cancel 按钮。查看 Button1_Click 反编译后的代码,果不其然:
从这里可以看出来,显示完提示之后,会打开 Form2(Form2.Show()),该类在 USB_Dumper.My.MyProject
中,获取时会返回一个 USB_Dumper.Form2
的实例:
终于在该类找到关键方法 WndProc()
,WndProc 是窗口过程函数,当程序窗口收到一个 OS 消息后就会调用该函数
分析 WndProc
先来看检测到 U 盘插入时的代码:
U 盘插入的检测
第一句 if (M.Msg == 0x219)
在判断到来的消息是不是 WM_DEVICECHANGE
(值为 0x219),当设备发生变更时(设备拔插、设备配置信息更改等情况)系统会向每个窗口发送 WM_DEVICECHANGE 消息,并通过携带的 wParam 与 lParam 参数来便于进程判断设备变更的原因及相关数据,这是由 Windows 的消息机制决定的
1 | IntPtr wParam = M.WParam; |
接着获取 wParam,并判断是否等于 0x8000,可以在 微软 API 文档 中查询 WM_DEVICECHANGE 来查看 wParam 各种的值代表什么意思:
0x8000 对应 DBT_DEVICEARRIVAL
,表示本次设备变更的原因是 有一个可用的设备接入了,下面的 0x8004 后边会用到,0x8004 表示 有一个设备移除了
接着 if (Marshal.ReadInt32(M.LParam, 4) == 2)
判断从 lParam 中读取到的值是否为 2。根据 API 文档 得知,此处的 lParam 是一个 DEV_BROADCAST_HDR 结构体指针:
1 | typedef struct _DEV_BROADCAST_HDR { |
Marshal.ReadInt32(IntPtr, Int32)
函数的作用是 从 IntPtr 指向的内存中按给定的偏移量 Int32 读取一个 32 位带符号整数,这里的 Marshal.ReadInt32(M.LParam, 4) 表示从 LParam 指向的内存地址处偏移 4 字节读取一个 DWORD,即 DEV_BROADCAST_HDR.dbch_devicetype,开发人员根据该值来判断设备类型,从而将 DEV_BROADCAST_HDR 结构体转换为 各设备对应的设备管理结构体
这里判断表示 dbch_devicetype 是否为 2,即设备的类型是否为逻辑卷:
如果是逻辑卷,就将 DEV_BROADCAST_HDR 结构体转换为 DEV_BROADCAST_VOLUME:
1 | DEV_BROADCAST_VOLUME dev_broadcast_volume = new DEV_BROADCAST_VOLUME(); |
文档 中也可以找到 DEV_BROADCAST_VOLUME 的定义:
1 | typedef struct _DEV_BROADCAST_VOLUME { |
再接着又是一个判断 if (dev_broadcast_volume.Dbcv_Flags == 0)
,由下图可知,表示本次变更受到影响的是物理的设备或驱动:
综合四个 if 判断:
- M.Msg == 0x219 -> 消息类型 == WM_DEVICECHANGE(设备发生变更)
- wParam == 0x8000 -> 设备变更原因 == DBT_DEVICEARRIVAL(设备接入)
- Marshal.ReadInt32(M.LParam, 4) == 2 -> 设备类型 == Logical volume(逻辑卷)
- dev_broadcast_volume.Dbcv_Flags == 0 -> 本次变更受到影响的是物理的设备或驱动
可以得出结论,有物理可用的移动逻辑卷设备接入
查找 U 盘盘符
接下来要做的事就是找到该设备对应的盘符,然后就可以进行文件拷贝了:
1 | int num2 = 0; |
程序通过一个 do-while 循环来查找接入设备的盘符,Dbcv_Unitmask 是刚提到的 DEV_BROADCAST_VOLUME 结构体的一个成员:
该字段是一个掩码,用于标记当前设备的盘符,当最低位(Bit 0)为 1,其他位都为 0 时,表示该设备是 A 盘,同理,当 Bit 1(位 1)标记为 1 时,表示该设备是 B 盘
Math.Pow(2.0, (double) num2)
即 2 的 num2 次方,换种写法就是 1 << num2,所以这个循环是在找 Dbcv_Unitmask 的哪一位是 1(并设置了一个搜索上限 20)。找到后,用左移的位数加上 0x41(’A’)得到目标盘符的 ascii 码,转换成 string 后(Conversions.ToString)与 :\* 拼接。如果找到 U 盘的盘符是 F,则拼接得到 F:\*,表示 U 盘中的所有内容
文件拷贝
最后判断 C:\USB 目录是否存在(DirectoryExists),不存在就创建(CreateDirectory),然后通过静态方法 Interaction.Shell
执行 xcopy 命令来拷贝文件:
Interaction.Shell
函数有 4 个参数
- Pathname:要执行的程序名以及任何需要的参数和命令行开关
- Style:要运行的程序的窗口样式
- AppWinStyle.Hide 隐藏窗口并为隐藏的窗口提供焦点
- AppWinStyle.NormalFocus 为窗口提供焦点,并以最近的大小和位置显示窗口
- AppWinStyle.MinimizedFocus 为窗口提供焦点,并以图标的形式显示窗口
- AppWinStyle.MaximizedFocus 为窗口提供焦点,并以全屏方式显示窗口
- AppWinStyle.NormalNoFocus 将窗口设置为最近的大小和位置。当前活动窗口保持焦点
- AppWinStyle.MinimizedNoFocus 以图标的形式显示窗口。当前活动窗口保持焦点
- Wait:指示 Shell 函数是否应等待程序完成的值
- Timeout:Wait 为 true 时该参数指定等待完成的毫秒数。如果将它设置为 -1,则 Shell 等到程序完成才返回
假设上一步拼接得到的 str 为 F:\*
,则将要执行的命令为 xcopy F:\* /y /q /h /e /i C:\USB
xcopy
命令参数
- /y:禁止提示确认要覆盖已存在的目标文件
- /q:禁止显示 xcopy 的消息
- /h:复制具有隐藏和系统文件属性的文件
- /e:复制所有子目录,包括空目录
- /i:如果目标路径不存在,xcopy 将依据给定的目标路径创建一个新目录
现在,USB Dumper 的原理就已经昭然若揭了 —— 监听设备变更消息,如果有物理可用的移动逻辑卷设备接入,找到其盘符,拷贝文件
检测 U 盘移除
WndProc 函数剩下的代码如下,用于处理 U 盘退出事件:
0x8004 表示有设备移除了,后面的代码与之前分析的基本相同。不过这里只取到了移除设备的盘符,放在 str4 中,没有进行其他操作
攻击方改进思考
- 可以考虑在设备移除时判断移除的是否为之前插入的设备,如果是,则将 C:\USB 目录下的文件通过网络发送给攻击者(具体实现起来还可以对 C:\USB 的目录结构进行管理,防止反复发送重复数据,增加指定文件后缀传输功能也是不错的)
- 该进程仍然可以在任务管理器中被发现,可以考虑 hook Win API 来隐藏 USB Dumper 进程
- C:\USB 目录太过明显,可以考虑生成随机字符串作为目录名,并将该目录藏得越深越好
- 定期删除备份目录下的文件(成功发送给攻击者的那部分),并根据磁盘剩余容量情况动态更改备份目录路径
防御方思路借鉴
- 这个思路也可以用于检测 U 盘是否携带病毒,当检测到 U 盘插入,检查目录下是否有如 autorun 等可疑文件。如果有,则用 API 强制弹出 U 盘并提醒用户
- 当然也可以加入加/解密模块,做成只有当指定 U 盘插入时,才会解密计算机上的文件的效果,当 U 盘拔出,又将文件加密存储
PS. 基于这个原理,我也实现了一个 USB Dumper 的 C++ GUI 版本,代码开放在 github,仅供学习交流使用