最近公司有这样的一个需求,即老板要在他的电脑上控制同网段内的其它电脑上的USB存储设备是否可用,在一定程度上防止资料外泄。看到这里也许有人会说了,不让用U盘,直接发QQ文件或者邮件不就行了吗?哈哈,这些电脑都没有连接外网!!!好了,废话不多说。
刚拿到这个需求的时候,第一反应,是到网上找看有没有现成的东西,万能的度娘还真找到一个,哇~收费的!只能自己开发了。
先分析需求,通过查资料,比较容易实现的有两种方式,一、修改注册表键值,二、删除USB存储设备驱动,并且清除USB使用记录。知道了怎么禁用USB存储设备,就开始着手做了,共分下边几步:
1、查找出同网段在线计算机
2、服务端发送相应指令给客户端
3、客户端执行操作并反馈状态
大的来说是这样的实现原理,但是中间涉及到几个问题
1、通讯问题,之前没有接触过socket,这是第一次做。最开始想的是只有一个服务端,其余都是客户端。服务端即老板的计算机,客户端即员工的计算机,当员工电脑开机的时候,主动请求老板的电脑,并保持长连接。这样做貌似很复杂的样子,没办法,只能怪学艺不精了。所以只好换了一种方式,一个客户端,对应N个服务端。客户端即老板的计算机,服务端即员工的计算机,这样,只要员工的计算机在线并且能ping通,老板需要设置的时候,直接连接即可。为了顺应思路,以下所说的客户端均指的是员工电脑(真正的服务端),服务端指的是老板的电脑(真正的客户端)。
2、客户端上的软件表现形式。最开始想到的是放到桌面右下角的托盘区域,但是想想觉得不妥,这样做比较直观,需要防止员工退出软件。后来想着试试做成windows服务,因为不知道做成服务以后,socket还能不能用,只能先试试,结果发现木有问题
3、怎么修改注册表键值,之前没有做过,后来问的度娘
4、最头疼的问题,即清理注册表USB存储设备使用记录的问题。win7和XP系统的不同之处在于,权限系统的变更导致的对注册表敏感数据的操作没有权限。在百度上搜索了好多文章,最后找到一个方法,即psexec.exe,可以以system权限运行软件,操作敏感数据。psexec.exe是命令行启动的,附带了一堆参数,具体参数都是什么含义请自行百度,我用到的就是 psexec.exe -i -d -s xx.exe这个命令,这就引出下一个问题了
5、c# winform 怎么执行命令行代码,我去,还得找万能的度娘
解决了以上问题,只完成了最基本的需求,即可以以注册表的方式、驱动的方式禁用、启用USB存储设备。需要完善的地方:
1、端口等的配置,我做的时候端口是写死的
2、控制端权限,现在我没做权限,打开直接运行,谁都可以使用
3、不能查询受控端计算机当前状态是否禁用,这个很好实现,有别的项目了,懒得做了
4、怎么防止别人修改注册表或者还原驱动程序,现在能想到的是定时查询,如果被还原了再次设置
5、控制端没有保存当前设置以供受控端查询比对,这个实现也不难
6、受控端退出服务以后失效,再做一个服务,用以保护主服务,当主服务被停止的时候,自动启动主服务
由于新项目,待以后有时间了再做完善吧
下面放出控制端截图:
1、查找计算机:
网上的方式多种多样,我用了一个最快的方式,但不是最保险的,即ping的方式,速度很快,只列出在线计算机的IP地址。缺点是,如果对方计算机屏蔽了ping就废了,不过好在都是内网的,可控。
实现代码如下:
<span style="white-space:pre"> </span>private void EnumComputers()
{
listBox1.Items.Clear();
try
{
string startIP = textBox1.Text.Trim();
string endIP = textBox2.Text.Trim();
if(!checkIP(startIP) || !checkIP(endIP))
{
MessageBox.Show("开始IP或者结束IP格式错误");
return;
}
string[] startIPArr = startIP.Split('.');
string[] endIPArr = endIP.Split('.');
int startNum = Convert.ToInt32(startIPArr[3]);
int endNum = Convert.ToInt32(endIPArr[3]);
for (int i = startNum; i <= endNum; i++)
{
Ping myPing;
myPing = new Ping();
string pingIP = startIPArr[0] + "." + startIPArr[1] + "." + startIPArr[2] + "." + i;
myPing.SendAsync(pingIP, 1000, null);
//PingReply pr = myPing.Send(pingIP, 1000, null);
myPing.PingCompleted += new PingCompletedEventHandler(_myPing_PingCompleted);
//if (pr.Status==IPStatus.Success)
// listBox1.Items.Add(pingIP);
}
}
catch
{
}
}
private void _myPing_PingCompleted(object sender, PingCompletedEventArgs e)
{
if (e.Reply.Status == IPStatus.Success)
{
if (!listBox1.Items.Contains(e.Reply.Address.ToString()))
listBox1.Items.Add(e.Reply.Address.ToString());
}
}
正则匹配IP地址的
<span style="white-space:pre"> </span>private bool checkIP(string str)
{
string pattern = @"\d+\.\d+\.\d+\.\d+";
Match m = Regex.Match(textBox1.Text, pattern);// 匹配正则表达式
if (m.Success)
return true;
else
return false;
}
下边是控制端的核心了,socket通信部分,不要看我的事件名称有汉字,这个是c# winform 的contextMenuStrip自动生成的,懒得改,不要吐槽啊!
<span style="white-space:pre"> </span>private void 启用USBToolStripMenuItem_Click(object sender, EventArgs e)
{
if (listBox1.SelectedItem != null && listBox1.SelectedItems.Count > 0)
{
for (int i = 0; i < listBox1.SelectedItems.Count; i++)
{
if (!checkValue(textBox3.Text.Trim()))
{
MessageBox.Show("端口号只能为正整数");
return;
}
int myProt = Convert.ToInt32(textBox3.Text.Trim());
//设定服务器IP地址
string setIP = listBox1.SelectedItems[i].ToString();
IPAddress ip = IPAddress.Parse(setIP);
Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
clientSocket.Connect(new IPEndPoint(ip, myProt)); //配置服务器IP与端口
//Console.WriteLine("连接服务器成功");
richTextBox1.Text += "IP为 " + setIP + "的客户端连接成功\r\n";
//通过clientSocket接收数据
int receiveLength = clientSocket.Receive(result);
//Console.WriteLine("接收服务器消息:{0}", Encoding.ASCII.GetString(result, 0, receiveLength));
richTextBox1.Text += "IP为 " + setIP + " 的客户端返回状态:" + Encoding.ASCII.GetString(result, 0, receiveLength) + "\r\n";
string sendIp = "3";
clientSocket.Send(Encoding.ASCII.GetBytes(sendIp));
//Console.WriteLine("向服务器发送消息:" + sendIp);
richTextBox1.Text += "向IP为 " + setIP + " 的客户端发送指令:启用USB(注册表)\r\n";
richTextBox1.Text += "向IP为 " + setIP + " 的客户端发送完毕" + "\r\n";
//Console.WriteLine("发送完毕,按回车键退出");
//Console.ReadLine();
receiveLength = clientSocket.Receive(result);
string msg = Encoding.ASCII.GetString(result, 0, receiveLength);
if (msg == "1")
richTextBox1.Text += "IP为 " + setIP + " 的客户端设置成功" + "\r\n\r\n";
else
richTextBox1.Text += "IP为 " + setIP + " 的客户端设置失败" + "\r\n\r\n";
}
catch (Exception err)
{
richTextBox1.Text += "IP为 " + setIP + " 的客户端连接失败," + err.Message + "\r\n";
//Console.WriteLine("连接服务器失败," + err.Message + ",请按回车键退出!");
//Console.ReadLine();
}
finally
{
if (clientSocket != null)
clientSocket.Close();
}
}
}
else
{
MessageBox.Show("请选择远程主机");
}
}
控制端的核心代码就这些,具体请自己实现。界面上,用的到的控件有 textbox listbox richtextbox button 以及右键菜单的 contextMenuStrip
下边说说受控端的windows 服务
不知道怎么创建windows 服务的请自行解决,这些不是本文讨论的重点,不做赘述。
首先,在OnStart里启用连接监听,这样在控制端连接的时候,会发送握手信息,并且接收控制端的指令,具体代码如下
<span style="white-space:pre"> </span>protected override void OnStart(string[] args)
{
int myProt = 61231;
//服务器IP地址
string setIP = getIP();
IPAddress ip = IPAddress.Parse(setIP);
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(new IPEndPoint(ip, myProt)); //绑定IP地址:端口
serverSocket.Listen(10); //设定最多10个排队连接请求
Console.WriteLine("启动监听{0}成功", serverSocket.LocalEndPoint.ToString());
//通过Clientsoket发送数据
Thread myThread = new Thread(ListenClientConnect);
myThread.Start();
Console.ReadLine();
}
/// <summary>
/// 监听客户端连接
/// </summary>
private static void ListenClientConnect()
{
while (true)
{
Socket clientSocket = serverSocket.Accept();
clientSocket.Send(Encoding.ASCII.GetBytes("Server Say Hello"));
Thread receiveThread = new Thread(ReceiveMessage);
receiveThread.Start(clientSocket);
}
}
接收指令代码如下:
<span style="white-space:pre"> </span>/// <summary>
/// 接收消息
/// </summary>
/// <param name="clientSocket"></param>
private static void ReceiveMessage(object clientSocket)
{
Socket myClientSocket = (Socket)clientSocket;
try
{
//通过clientSocket接收数据
int receiveNumber = myClientSocket.Receive(result);
//Console.WriteLine("接收客户端{0}消息{1}", myClientSocket.RemoteEndPoint.ToString(), Encoding.ASCII.GetString(result, 0, receiveNumber));
string clientMsg = Encoding.ASCII.GetString(result, 0, receiveNumber);
//Console.WriteLine(clientMsg);
//wirteLog(clientMsg);
//获取到传递的数据
if (clientMsg == "4")
{
bool a = RegToStopUSB();
if (a)
//ListenClientConnect("1");
myClientSocket.Send(Encoding.ASCII.GetBytes("1"));
else
myClientSocket.Send(Encoding.ASCII.GetBytes("0"));
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
myClientSocket.Shutdown(SocketShutdown.Both);
myClientSocket.Close();
}
}
然后根据接收到的指令,调用不同的函数来执行不同的操作,代码如下:
<span style="white-space:pre"> </span>/// <summary>
/// 禁用USB
/// </summary>
/// <returns>状态</returns>
private static bool RegToStopUSB()
{
try
{
RegistryKey regKey = Registry.LocalMachine;
string keyPath = @"SYSTEM\CurrentControlSet\Services\USBSTOR";
RegistryKey openKey = regKey.OpenSubKey(keyPath, true);
openKey.SetValue("Start", 4);
openKey.Close();
return true;
}
catch (Exception ex)
{
throw ex;
}
}
/// <summary>
/// 启用USB
/// </summary>
/// <returns>状态</returns>
private static bool RegToRunUSB()
{
try
{
RegistryKey regKey = Registry.LocalMachine; //读取注册列表HKEY_LOCAL_MACHINE
string keyPath = @"SYSTEM\CurrentControlSet\Services\USBSTOR"; //USB 大容量存储驱动程序
RegistryKey openKey = regKey.OpenSubKey(keyPath, true);
openKey.SetValue("Start", 3); //设置键值对(3)为开启USB(4)为关闭
openKey.Close(); //关闭注册列表读写流
return true;
}
catch (Exception ex)
{
throw ex;
}
}
<span style="white-space:pre"> </span>/// <summary>
/// 备份驱动
/// </summary>
/// <returns></returns>
private static int backFile()
{
try
{
if (!Directory.Exists("d:\\pic"))
Directory.CreateDirectory(@"C:\Windows\infBack\");
if (File.Exists(@"C:\Windows\inf\usbstor.inf") && File.Exists(@"C:\Windows\inf\usbstor.PNF"))
{
string fromFile = @"C:\Windows\inf\usbstor.inf";
string toFile = @"C:\Windows\infBack\usbstor.inf";
if (File.Exists(fromFile))
File.Copy(fromFile, toFile, true);
fromFile = @"C:\Windows\inf\usbstor.PNF";
toFile = @"C:\Windows\infBack\usbstor.PNF";
if (File.Exists(fromFile))
File.Copy(fromFile, toFile, true);
return 1;
}
else
{
return -2;//驱动文件不存在
}
}
catch (Exception err)
{
return -1;//错误
}
}
<span style="white-space:pre"> </span>/// <summary>
/// 还原驱动
/// </summary>
/// <returns></returns>
private static int restoreFile()
{
try
{
if (Directory.Exists(@"C:\Windows\infBack\") && File.Exists(@"C:\Windows\infBack\usbstor.inf") && File.Exists(@"C:\Windows\infBack\usbstor.PNF"))
{
string fromFile = @"C:\Windows\infBack\usbstor.inf";
string toFile = @"C:\Windows\inf\usbstor.inf";
File.Copy(fromFile, toFile, true);
fromFile = @"C:\Windows\infBack\usbstor.PNF";
toFile = @"C:\Windows\inf\usbstor.PNF";
File.Copy(fromFile, toFile, true);
return 1;
}
else
{
return -10;//备份目录不存在
}
}
catch (Exception err)
{
return -1;
}
}
<span style="white-space:pre"> </span>/// <summary>
/// 删除U盘记录
/// </summary>
private static bool delRegUsbLog()
{
try
{
RegistryKey key = Registry.LocalMachine;
RegistryKey sunKey = key.OpenSubKey(@"SYSTEM\ControlSet001\Enum\USBSTOR", true);
string[] sunKeyNames = sunKey.GetSubKeyNames();
foreach (string name in sunKeyNames)
{
if (name.Contains("Disk&"))
{
//删除注册表项
sunKey.DeleteSubKeyTree(name + @"\", false);
}
}
//============================================================//
RegistryKey sunKey1 = key.OpenSubKey(@"SYSTEM\ControlSet002\Enum\USBSTOR", true);
string[] sunKeyNames1 = sunKey1.GetSubKeyNames();
foreach (string name in sunKeyNames1)
{
//删除注册表项
sunKey1.DeleteSubKeyTree(name + @"\", false);
}
//============================================================//
RegistryKey sunKey2 = key.OpenSubKey(@"SYSTEM\CurrentControlSet\Enum\USBSTOR", true);
string[] sunKeyNames2 = sunKey2.GetSubKeyNames();
foreach (string name in sunKeyNames2)
{
//删除注册表项
sunKey2.DeleteSubKeyTree(name + @"\", false);
}
//============================================================//
RegistryKey sunKey3 = key.OpenSubKey(@"SYSTEM\ControlSet001\Enum\USB", true);
string[] sunKeyNames3 = sunKey3.GetSubKeyNames();
foreach (string name in sunKeyNames3)
{
if (name.Contains("VID_"))
{
//删除注册表项
sunKey3.DeleteSubKeyTree(name + @"\", false);
}
}
//============================================================//
RegistryKey sunKey4 = key.OpenSubKey(@"SYSTEM\ControlSet002\Enum\USB", true);
string[] sunKeyNames4 = sunKey4.GetSubKeyNames();
foreach (string name in sunKeyNames4)
{
if (name.Contains("VID_"))
{
//删除注册表项
sunKey4.DeleteSubKeyTree(name + @"\", false);
}
}
//============================================================//
RegistryKey sunKey5 = key.OpenSubKey(@"SYSTEM\CurrentControlSet\Enum\USB", true);
string[] sunKeyNames5 = sunKey5.GetSubKeyNames();
foreach (string name in sunKeyNames5)
{
if (name.Contains("VID_"))
{
//删除注册表项
sunKey5.DeleteSubKeyTree(name + @"\", false);
}
}
return true;
}
catch (Exception err)
{
return false;
}
}
以上是windows 服务的核心代码。服务写完以后,刚开始是使用了打包安装的方式,发现有个问题,就是在安装完成以后就要清理注册表、备份驱动、删除驱动、启动服务等等,觉得这样做不方便,于是又写了一个安装程序
安装程序需要复制文件、安装服务、启动服务、设置注册表禁用USB存储设备、删除注册表USB存储设备使用记录、备份USB存储设备驱动、删除USB存储设备驱动,这就需要执行cmd命令行、执行.bat批处理等等,简直太虐了,只能一点一点来了,还是先贴完成截图:
点击 开始安装 以后,会执行以下步骤
1、复制文件到相应文件夹中
2、执行 psexec.exe -i -d -s usbSetup.exe 命令,以system权限打开usbSetup,执行安装服务、启动服务、设置注册表键值、删除注册表中USB存储设备使用记录、备份驱动文件、删除驱动驱动文件等一系列操作
具体代码如下:
开始安装 按钮事件
<span style="white-space:pre"> </span>private void button1_Click(object sender, EventArgs e)
{
//开始安装
button1.Text = "正在安装...";
button1.Enabled = false;
//richTextBox1.Text += "正在展开文件...\r\n";
writeLog("正在展开文件...");
//服务程序目标路径
string toPath = @"C:\windows\usbServer\";
//服务程序文件路径
string fromPath = Application.StartupPath + "\\usbServer";
copyFile(fromPath, toPath);
int isNet = 0;
List<string> list = GetDotNetVersions();
for (int i = 0; i < list.Count; i++)
{
if (list[i].Contains("4.0"))
isNet = 1;
}
if (isNet == 0)
{
//安装.net
System.Diagnostics.Process.Start(Application.StartupPath + @"\.net\dotNetFx40_Client_x86_x64.exe").WaitForExit();
//安装.net语言包
System.Diagnostics.Process.Start(Application.StartupPath + @"\.net\dotNetFx40LP_Client_x86_x64zh-Hans.exe").WaitForExit();
}
//执行cmd,打开真正的安装程序
string installStr = Application.StartupPath + @"\psTools\run.bat";
Cmd(installStr);
this.Close();
Application.Exit();
}
<span style="white-space:pre"> </span>/// <summary>
/// 执行cmd命令
/// </summary>
/// <param name="c">要执行的命令</param>
public void Cmd(string c)
{
System.Diagnostics.Process process = new System.Diagnostics.Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardInput = true;
process.Start();
process.StandardInput.WriteLine(c);
process.StandardInput.AutoFlush = true;
process.StandardInput.WriteLine("exit");
//StreamReader reader = process.StandardOutput;//截取输出流
//string output = reader.ReadLine();//每次读取一行
//while (!reader.EndOfStream)
//{
// //PrintThrendInfo(output);
// writeLog(output);
// output = reader.ReadLine();
//}
process.WaitForExit();
}
复制文件
<span style="white-space:pre"> </span>private void copyFile(string fromPath,string toPath)
{
try
{
//检查目标目录是否以目录分割字符
//结束如果不是则添加之
if (toPath[toPath.Length - 1] != Path.DirectorySeparatorChar)
toPath += Path.DirectorySeparatorChar;
//判断目标目录是否存在如果不存在则新建之
if (!Directory.Exists(toPath))
Directory.CreateDirectory(toPath);
//得到源目录的文件列表,该里面是包含
//文件以及目录路径的一个数组
//如果你指向copy目标文件下面的文件
//而不包含目录请使用下面的方法
//string[]fileList= Directory.GetFiles(srcPath);
string[] fileList =
Directory.GetFileSystemEntries(fromPath);
//遍历所有的文件和目录
foreach (string file in fileList)
{
//先当作目录处理如果存在这个
//目录就递归Copy该目录下面的文件
if (Directory.Exists(file))
copyFile(file,toPath + Path.GetFileName(file));
//否则直接Copy文件
else
{
File.Copy(file, toPath + Path.GetFileName(file), true);
writeLog("正在展开--->" + Path.GetFileName(file));
}
}
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
}
<span style="white-space:pre"> </span>/// <summary>
/// 写入状态
/// </summary>
/// <param name="str">要显示的值</param>
private void writeLog(string str)
{
richTextBox1.Text += str + "\r\n";
richTextBox1.Focus();
}
<span style="white-space:pre"> </span>/// <summary>
/// 获取已经安装的.net版本
/// </summary>
/// <returns></returns>
private List<string> GetDotNetVersions()
{
DirectoryInfo[] directories = new DirectoryInfo(
Environment.SystemDirectory + @"\..\Microsoft.NET\Framework").GetDirectories("v?.?.*");
List<string> list = new List<string>();
foreach (DirectoryInfo info2 in directories)
{
list.Add(info2.Name.Substring(1));
}
return list;
}
以上代码是初始化安装界面,还有一个真正的安装界面,样子和这个一样,但是执行的代码可是不一样,具体代码如下:
<span style="white-space:pre"> </span>/// <summary>
/// 安装服务
/// </summary>
private void installServer()
{
writeLog("正在安装服务...");
string cmdStr = @"%SystemRoot%\Microsoft.NET\Framework\v4.0.30319\installutil.exe c:\windows\usbServer\usbService.exe";
Cmd(cmdStr);
writeLog("安装成功!");
writeLog("正在启动服务...");
cmdStr = @"Net Start usbService";
Cmd(cmdStr);
writeLog("启动服务成功!");
writeLog("开始清理缓存文件...\r\n");
t1 = new Thread(new ThreadStart(delRegUsbLog));
t1.Start();
}
其它的代码,比如注册表禁用、启用、清理USB使用记录、备份驱动、删除驱动上边已经贴出相关代码了,不再赘述
这里需要注意一点的是,因为是要在richtextbox里实时显示状态的,所以用到了线程,需要在窗体的load事件里加入如下代码
Control.CheckForIllegalCrossThreadCalls = false;
该处执行cmd命令的函数使用的是上边的public void Cmd(string c),区别是放开了注释的部分,用于实时显示状态
另外,那个批处理的代码如下:
@echo off
set a= %cd%
echo %a%
set b=%a:psTools=install%
echo %b%
set c= %a%\psTools\psexec.exe -i -d -s %b%\install\usbSetup
echo %c%
start %c%
至此,核心代码基本完成。完整代码因为种种原因不能往外放,只要自己用点心可以拿核心代码拼装一个。
总结一下,做这个东西用了一天半的时间。其中,比较耗费时间的是找思路,怎么做这件事。另外,socket没有接触过,都是从网上找的现成代码,使用之后发现了个bug,cpu使用率会飚到100%,自己改了一下。安装的时候清理USB存储设备使用记录的权限问题,也耗费了不少脑细胞,最后找到psexec.exe这个工具,想要的去百度,然后到微软官方下载,就不放具体下载地址了。这个小软件除了本文一开始描述的需要完善的地方以外,满足我们的需求是没有问题的。
特别说明,注册表禁用USB、注册表USB存储设备使用记录、USB存储设备驱动文件位置请自行百度,答案一堆一堆的, 写了这么多,实在是懒得贴了,见谅见谅!
此文仅用于记录及回顾,也给有这方面需求的朋友一个思路,如果有更好的方案或者有错误的地方,欢迎批评指正!