0x00 Windows批处理脚本优势
- 不需要安装各种编译解释器,Windows下直接使用
- 自动化处理,解决windows下的重复操作,提高工作效率
- 简单易学习
0x01 需求背景
为测试一个系统处理速度性能。需要编写所有样本(样本均为.db文件)的一个描述文件(decription.temp)作为输入,只要描述文件存在,系统立即根据描述文件描述的文件属性(例如样本存储的路径)进行对应的样本处理。处理结束后会在另一个指定文件夹中输出一个结果文件(例如:result.txt)。然而该系统无源码,无法插桩记录开始和结束时间并进行时间效率计算。考虑整个系统的过程为输入描述文件到输出结果文件。只要记录下描述文件被放入输入文件夹的开始时间和产生输出文件的结束时间即可。
0x02 总体思路
由于要实现功能的比较简单,我们采用bat批处理进行实现。该批处理需要实现的功能步骤如下所示:
- 研究描述文件格式,提取出描述一个样本文件的模板字符串,并放入template.txt中。
- 遍历所有db文件,并根据template.txt中的模板构造文件描述字符串,并将其附加到decription.txt。
- 更改decription.txt后缀名为decription.temp,让系统立即开始处理。
- 第3步同时,获取系统时间并记录下来。
- 循环访问指令目录下是否产生了result.txt文件。若产生则第6步。
- 记录结束时的系统时间。
- 根据记录的开始时间和结束时间计算总耗时和效率。
0x03 bat批处理脚本实现
本节将按照上节每个步骤依次进行讲解和实现。
1、构造template.txt文件
通过研究描述文件格式,发现不同样本只需要写入一个相似的字符串到描述文件中。每个文件的描述字符串仅仅是文件全路径不同。所以我们任意提取其中一个文件的描述字符串并把其中的文件名变为ZWF.db,意为占位符。因为我们的样本也放入该文件夹下,所以路径我们就不动了。把该模板字符串手动存储到template.txt中,供批处理程序读取并使用。模板为下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/88374521820d3fd23afa4c9b96ed8a08.png)
2、构造decription.txt文件
构造描述文件需要按照模板字符串对所有样本进行构造,首先获取模板字符串,代码如下所示:
rem 测试计算某系统识别效率
@echo off
setlocal enabledelayedexpansion
::获取txt中的模板字符串
for /f "delims=" %%b in (template.txt) do (
set template=%%b
goto A
)
:A
::遍历所有db文件并构造描述文件
set dbNum=0
set FolderName=.
for /f "delims=" %%a in ('dir /b "%FolderName%\*.db"') do (
set newTemp=%template%
set var=!newTemp:ZWF.db=%%a!
echo !var!>>decription.txt
set /a dbNum+=1
)
echo 文件夹中总共有:!dbNum!个文件
运行后生成如下文件:
![](https://i-blog.csdnimg.cn/blog_migrate/4e51e64e92ae0c077ccb184cab6dd4df.png)
与预想的效果一致。
此处代码我们需要了解以下批处理脚本知识。
2.1、注释
任何高级语言都会存在注释,提高代码可阅读性,windows 批处理也不例外。在批处理中存在两种注释方法:
- rem 注释
- :: 注释
例如上述构造decription.txt文件脚本中这两种注释均被使用到。
2.2、echo输出语句
echo可作为控制台输出语句,格式为:echo "String"
echo on:若定义此句,后面所有脚本命令将会显示出来,bat中是默认打开的。这种视觉体验不好。
echo off:关闭回显,和echo on功能相反,将表明不会显示所执行的命令。但本身会被显示。
@echo off:加上@除了上述外,“echo off”本身也不会显示。也就是只显示echo指示的;内容。
直接看下图就很容易理解:
![](https://i-blog.csdnimg.cn/blog_migrate/a8e3440786d24aa96bb649e648adedfc.png)
2.3 goto跳转语句
使用很简单:
goto A
//some command
:A
运行到goto时,将直接跳转到指定标签后的第一条命令开始执行。例如上述代码中为了只获取第一行的模板字符串,运行一次就直接goto到for循环外。
2.4 变量定义与使用
1、定义
windows批处理中定义变量的方法为:
set var=1
变量名为var,值为1。批处理脚本变量类型为弱类型。
2、获取值
若要获取变量值使用:%var%。例如:
set var1=%var%
var1的值将和var的值相同,理解很容易%var%相当于C中对指正的取值操作*ptr,而本身是个引用。对var变量的理解有助于在批处理定义函数中参数的使用方法,见函数定义一节的解释。
3、值与引用
批处理中定义的一个变量,没有像C++中对象赋值的功能。例如下面代码和显示:
![](https://i-blog.csdnimg.cn/blog_migrate/dcdba77742fe79f36f52ad93c13d96f4.png)
上图你看到,定义了一个变量var为5。本意试图将var1变量也指向var,但是上述语句将var直接解释成了一个值给var1。所以只能通过%var%获取值并赋值。
变量名作为引用可在函数参数中使用,见函数定义小节。
4、数值操作
若要进行+、+=等数值运算,在set中加入/a开关。 把等式右侧使用数值计算进行解析。例如:
set /a var3=%var%+1
/a表明=右侧是个数值计算,首先通过%取变量var的值,再将其加上1,结果赋值给var3。
5、延迟变量扩展
批处理脚本在运行前会进行预处理,如果某个变量在开始处被赋值,则后续命令出现相同变量的地方将直接被替换成具体的值。然后从头开始解析命令。该过程叫做变量扩展,这无疑优化加速了代码的运行。
![](https://i-blog.csdnimg.cn/blog_migrate/9d49ecb4756304dff8865c59af549081.png)
批处理在运行前已经将var1变成了具体的值5。然而这也存在一个问题,请看如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/948b1a6a64ff7cb2b25f00c5dce49f35.png)
本意上var因为10,但是实际上为5。因为预处理是按行读取,当读到第二条指令时,由于var的重新赋值也在该行,预处理程序无法感知变量var的变化,所以错误的认为当前var肯定为之前设置的5,最后var将被替换成错误的值5被运行。
为了避免这种变量扩展的错误,批处理提供延迟变量扩展的语句,也就是在预处理中对变量不做处理,延迟到运行时感知变量的变化。只要添加一下语句:
setlocal enabledelayedexpansion
并在使用到需要变量延迟扩展的地方使用 !var! 表示。
例如上述程序加入该语句后将正确显示,如下图:
![](https://i-blog.csdnimg.cn/blog_migrate/dd537d12a6be33278c80bc54c999bf8e.png)
set命令见:
https://blog.csdn.net/actanble/article/details/59154972
2.4 dir命令
文件显示操作命令。dir命令显示如下:
若只显示文件名和扩展名加上/b参数
*.db过滤出后缀名为db的所有文件。详细的dir命令见:
https://www.cnblogs.com/tinaluo/p/8367442.html
2.5 字符串替换
批处理中字符串中字串的替换非常方便,格式为:
%StrVar:old=new%
将StrVar变量中子串old变为new(注意oldstr为出现在StrVar变量的子串)。
例如上述脚本,根据模板字符串替换其中的ZWF.db为特定国的文件名即可:
set var=!newTemp:ZWF.db=%%a!
字符串处理请见下文:
https://blog.csdn.net/u012516524/article/details/79752962?utm_source=blogxgwz9
2.6 文件写入
文件写入使用>或者>>,区别为:
echo !var!>>bcp.txt 不清除原有字符串,将字符串append到txt尾部。
echo !var!>bcp.txt 清除原有文件内容并写入新的内容。
> 清除文件中原有的内容后再写入
>> 追加内容到文件末尾,而不会清除原有的内容主要将本来显示在屏幕上的内容输出到指定文件中指定文件如果不存在,则自动生成该文件
2.7 for循环
for循环是任何语言中均存在的必要语句。在windows批处理脚本中for的用法很多,相对比较复杂,但是学会了,能够极大的提高访问的效率。因为其不仅拥有一般意义上for语句的作用,更重要的是能直接访问文件内容。
1、常见意义中的for循环
这种for循环的结构为:
for /L %%i in (start,step,end) do command
/L 表示列表循环
start:开始 end:结束 step:步长
每次循环获取的值将赋值%%i,函数体内部使用%%i来说去当前循环对应的值
举个例子,例如C++中的一个循环如下:
for(int i=0;i<=10;i+=2)
{
printf("当前值为:%d",i);
}
则在批处理中可以这样实现:
@echo off
for /L %%i in (0,2,10) do (
echo 当前值为:%%i
)
pause
2、/f参数的for循环
除了上述/L的for循环,批处理还提供了功能强大的/f的使用,其命令格式有以下三种用法:
FOR /f ["options"] %%i IN (file) DO command
其中file解释成文件,例如1.txt。每次循环将获取该文件中的一行并根据"options"的指定为%%i赋值该行中的特定数据。
例如上述代码的第一个for循环。获取了template.txt中的第一行赋值给了批处理脚本中的template变量。
FOR /F ["options"] %%i IN ("string") DO command
直接理解为字符串内容,将会根据"options"的指示分割
FOR /F ["options"] %%i IN ('command') DO command
'command'命令操作返回的文本作为参数 。例如上述第二个for循环,利用dir /b获取当前目录下的所有db文件名列表作为文件内容进行循环访问列表。
"options"有一下几个重要关键词:
“skip=n”:忽略前n行
“eol=a”: 忽略开头字符为a的行
“delims=,”:一行字符串通过,进行分割
"tokens=m,n ,o-q" :由于delims分割后分成多块,取其中的第m个放入%%i中,n放入%%j(隐含的,按照字母顺序递增) o到q个则放入%%k %%l %%m中。
推荐几个写的比较通俗易懂的文章:
for使用请见:
https://blog.csdn.net/sanqima/article/details/37884373?utm_source=blogxgwz7
options详情请见:
https://blog.csdn.net/sanqima/article/details/37884373?utm_source=blogxgwz7
这里我们使用批处理中的第一种和第三种for循环语句获取了文本文件中的第一行数据(也就是描述符模板字符串)。
3、更改后缀名
ren命令能够更改文件名,例如本文示例:
ren decription.temp.txt decription.temp
运行后文件decription.temp.txt将变为decription.temp
4、记录开始时间
::生成描述文件同时,系统开始处理,同时我们开始记录开始时间
set startTime=%time%
%time%为OS系统全局变量,可直接在程序中使用。
为了获取时间中的时、分、秒可以使用for语句中的delims和tokens的操作获取,见下文讲解。
![](https://i-blog.csdnimg.cn/blog_migrate/104c0f225412b30c1b02ad8980461f68.png)
环境变量也可以通过上述方式进行访问获取值。
其他可直接使用的变量很多,请见下文:
https://www.jb51.net/os/windows/57645.html
5、result.txt文件监测
代码如下:
::循环监测result.txt文件是否存在,若存在则跳出
:loop
if exist result.txt (
goto loop
)
5.1 if条件语句
这里if语句很容易能够理解,详细的用法见:
https://blog.csdn.net/sanqima/article/details/37818115
5.2 exist
exist 文件存在
not exist 不存在 not表示非
6、记录结束时间
和5一样。使用另一个变量进行保存即可。
set endTime=%time%
7、计算总时间
只要将结束时间减去开始时间就可以得到总耗时,时间格式为%time%形式,无法直接相减,我们定义一个函数用来计算一个给定%time%时间的总秒数,并调用,最后两个总秒数相减去。
set startAlltime=
set endAlltime=
call :caculatems %startTime% startAlltime
call :caculatems %endTime% endAlltime
::计算花费总时长
set /a spentTime=!endAlltime!-!startAlltime!
echo 总共花费时间:%spentTime% 秒
pause
::计算总秒数
:caculatems
for /f "tokens=2-3 delims=:." %%a in ("%~1") do (
set /a minutes=%%a*60
set /a seconds=%%b
)
set /a %2=%minutes%+%seconds%
goto :eof
7.1 函数的定义
函数定义格式为:
:functionName
rem 函数体功能
goto :eof
以一个标签开始,标签名即为函数名。函数以goto :eof结尾
调用函数使用
call :functionname var1 var2..
var1为参数列表
参数列表
和C++中一样,参数分为值引用和地址引用。
windows批处理带入参数直接在调用出
在批处理中如果函数体仅仅需要值即可,则带入%var%,运行后原var不变。
若想要一个返回值,则可以带入一个引用变量,也就是直接为var,函数运行后var值将改变。
例如上述脚本:
call :caculatems %startTime% startAlltime
%startTime%带入的是%time%格式的值,startAlltime是个引用,需要在函数内部通过操作计算%startTime%得到的总秒数赋给startAlltime。
参数使用
%1表示第一个参数 %2表示第二个参数
因为此处第二个参数是引用,所以可以直接使用这个语句:
set /a %2=%minutes%+%seconds%
有时候会看见%~1的使用,
%1就是表示批处理的第一个参数,
%~1表示删除参数外面的引号
比如有个批处理文件 test.bat
在cmd中输入命令
test.bat "test"
%1表示的是“test“。%~1表示的是test,没有了双引号
函数使用请见:
https://blog.csdn.net/xiaoding133/article/details/39252357
7.2 %time%的解析
因为%time%的格式,我们通过delims=:. 进行分割,并使用tokens=2-3 获取分钟和秒,代码见上述所示。这里的时间转换秒的解析使用了前文/f的for循环的第二种格式。
0x04 总体代码
rem 测试计算某系统识别效率
@echo off
setlocal enabledelayedexpansion
::获取txt中的模板字符串
for /f "delims=" %%b in (template.txt) do (
set template=%%b
goto A
)
:A
::遍历所有db文件并构造描述文件
set FolderName=.
set dbNum=0
for /f "delims=\" %%a in ('dir /b "%FolderName%\*.db"') do (
set newTemp=%template%
set var=!newTemp:ZWF.db=%%a!
echo !var!>>template.txt
set /a dbNum+=1
)
echo 文件夹中总共有:!dbNum!个文件
ren template.txt template.temp
::生成描述文件同时,系统开始处理,同时我们开始记录开始时间
set startTime=%time%
::循环监测result.txt文件是否存在,若存在则跳出
:loop
if exist result.txt (
goto loop
)
set endTime=%time%
set startAlltime=
set endAlltime=
call :caculatems %startTime% startAlltime
call :caculatems %endTime% endAlltime
::计算花费总时长
set /a spentTime=!endAlltime!-!startAlltime!
echo 总共花费时间:%spentTime% 秒
pause
::计算总秒数
:caculatems
for /f "tokens=2-3 delims=:." %%a in ("%~1") do (
set /a minutes=%%a*60
set /a seconds=%%b
)
set /a %2=%minutes%+%seconds%
goto :eof