软件下载地址:sourceforge

软件运行界面:

USBDumper运行界面

点击 Start 就弹出一个窗口提示 “USB Dumper have started !!! Close it Manually from the Task Manager !!!”,并开始在后台进行监听,可以通过任务管理器杀掉。当 U 盘插入时,就会自动将盘上的数据拷贝到 C:\USB 目录下(没有的话会自行创建)

peid 查了下发现是 .NET 框架的程序,反编译工具一开始选用的 dnSpy,后来发现在局部变量的处理上没有 Reflector 好,所以换成了后者,可惜的是不能加注释,也不能重命名方法和变量……


查找关键函数

在 USB_Dumper 的名称空间下看到一个 Main 类,点开又找到两个方法 Button1_ClickButton2_Click

猜测这两个方法对应软件主界面的 Start 和 Cancel 按钮。查看 Button1_Click 反编译后的代码,果不其然:

button1_click

从这里可以看出来,显示完提示之后,会打开 Form2(Form2.Show()),该类在 USB_Dumper.My.MyProject 中,获取时会返回一个 USB_Dumper.Form2 的实例:

Form2

终于在该类找到关键方法 WndProc(),WndProc 是窗口过程函数,当程序窗口收到一个 OS 消息后就会调用该函数

WndProc函数


分析 WndProc

先来看检测到 U 盘插入时的代码:

WndProc函数


U 盘插入的检测

第一句 if (M.Msg == 0x219) 在判断到来的消息是不是 WM_DEVICECHANGE(值为 0x219),当设备发生变更时(设备拔插、设备配置信息更改等情况)系统会向每个窗口发送 WM_DEVICECHANGE 消息,并通过携带的 wParam 与 lParam 参数来便于进程判断设备变更的原因及相关数据,这是由 Windows 的消息机制决定的

1
2
IntPtr wParam = M.WParam;
if (wParam == ((IntPtr) 0x8000))

接着获取 wParam,并判断是否等于 0x8000,可以在 微软 API 文档 中查询 WM_DEVICECHANGE 来查看 wParam 各种的值代表什么意思:

wParam

0x8000 对应 DBT_DEVICEARRIVAL,表示本次设备变更的原因是 有一个可用的设备接入了,下面的 0x8004 后边会用到,0x8004 表示 有一个设备移除了

接着 if (Marshal.ReadInt32(M.LParam, 4) == 2) 判断从 lParam 中读取到的值是否为 2。根据 API 文档 得知,此处的 lParam 是一个 DEV_BROADCAST_HDR 结构体指针:

1
2
3
4
5
typedef struct _DEV_BROADCAST_HDR {
DWORD dbch_size;
DWORD dbch_devicetype;
DWORD dbch_reserved;
} 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,即设备的类型是否为逻辑卷

lParam

如果是逻辑卷,就将 DEV_BROADCAST_HDR 结构体转换为 DEV_BROADCAST_VOLUME:

1
2
DEV_BROADCAST_VOLUME dev_broadcast_volume = new DEV_BROADCAST_VOLUME();
dev_broadcast_volume = (DEV_BROADCAST_VOLUME) Marshal.PtrToStructure(M.LParam, typeof(DEV_BROADCAST_VOLUME));

文档 中也可以找到 DEV_BROADCAST_VOLUME 的定义:

1
2
3
4
5
6
7
typedef struct _DEV_BROADCAST_VOLUME {
DWORD dbcv_size;
DWORD dbcv_devicetype;
DWORD dbcv_reserved;
DWORD dbcv_unitmask;
WORD dbcv_flags;
} DEV_BROADCAST_VOLUME;

再接着又是一个判断 if (dev_broadcast_volume.Dbcv_Flags == 0),由下图可知,表示本次变更受到影响的是物理的设备或驱动:

dbcv_flags

综合四个 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
2
3
4
5
6
7
8
9
int num2 = 0;
do {
if (Math.Pow(2.0, (double) num2) == dev_broadcast_volume.Dbcv_Unitmask) {
string str = Conversions.ToString(Strings.Chr(0x41 + num2)) + @":\*";
...
}
num2++;
num5 = 20;
} while (num2 <= num5);

程序通过一个 do-while 循环来查找接入设备的盘符,Dbcv_Unitmask 是刚提到的 DEV_BROADCAST_VOLUME 结构体的一个成员:

dbcv_unitmask

该字段是一个掩码,用于标记当前设备的盘符,当最低位(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 命令来拷贝文件:

code

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 盘退出事件:

code

0x8004 表示有设备移除了,后面的代码与之前分析的基本相同。不过这里只取到了移除设备的盘符,放在 str4 中,没有进行其他操作


攻击方改进思考

  1. 可以考虑在设备移除时判断移除的是否为之前插入的设备,如果是,则将 C:\USB 目录下的文件通过网络发送给攻击者(具体实现起来还可以对 C:\USB 的目录结构进行管理,防止反复发送重复数据,增加指定文件后缀传输功能也是不错的)
  2. 该进程仍然可以在任务管理器中被发现,可以考虑 hook Win API 来隐藏 USB Dumper 进程
  3. C:\USB 目录太过明显,可以考虑生成随机字符串作为目录名,并将该目录藏得越深越好
  4. 定期删除备份目录下的文件(成功发送给攻击者的那部分),并根据磁盘剩余容量情况动态更改备份目录路径

防御方思路借鉴

  1. 这个思路也可以用于检测 U 盘是否携带病毒,当检测到 U 盘插入,检查目录下是否有如 autorun 等可疑文件。如果有,则用 API 强制弹出 U 盘并提醒用户
  2. 当然也可以加入加/解密模块,做成只有当指定 U 盘插入时,才会解密计算机上的文件的效果,当 U 盘拔出,又将文件加密存储

PS. 基于这个原理,我也实现了一个 USB Dumper 的 C++ GUI 版本,代码开放在 github,仅供学习交流使用