Board logo

标题: [其他] 【讨论】CMD解释执行代码的流程 [打印本页]

作者: defanive    时间: 2011-8-23 03:40     标题: 【讨论】CMD解释执行代码的流程

本帖最后由 defanive 于 2011-8-24 00:24 编辑

由于打算动手做CMD,因此对CMD解释执行代码的流程必须有清晰正确的认识
开始查阅相关资料,在论坛中找到一个好帖:

[讨论]批处理中特殊字符的解释机制探索
http://www.bathome.net/viewthread.php?tid=12324

在此感谢帖子的作者,里面对预处理的一些解释顺序有了非常深入的研究

同时我也希望这个帖子对批处理也会有一些帮助
对CMD的执行流程越来越深入无疑是好处多过坏处的

注意:
仅代表个人意见,以及总结大家的结果
并不代表正确、官方的说法
有一定误导性,不能作为官方资料使用

2L会尝试整理出流程,希望大家能修正或者补全
3L会对2L中的一些流程进行补充说明、证据
4L会总结出目前还有的问题
作者: defanive    时间: 2011-8-23 03:41

本帖最后由 defanive 于 2011-8-23 04:26 编辑

未完成
PS:还有一些可以写的,困了跑去睡觉了

//主循环
//文件标记为0
{
  从标记处开始读取文件到结尾
  //第一步预处理
  {
    //处理%,^,"
    读取的内容从头至尾遍历
    {
      判断下一个字符
      {
        是%:
          判断%内解析标记
          {
            为真:
              将上一个%到本个%的内容为变量名,查找变量名。找到则替换为变量内容,未找到则将上一个%到本个%的内容丢弃
            为假:
              判断下一个字符
              {
                是数字:替换为单个命令行参数,即处理%0~%9的情况
                是*:替换为除%0命令行参数
                是~:继续判断是否合法,到不合法时终止。终止后,判断最后一位是否是数字,若是则替换为单个命令行参数
                是%:丢弃当前的%,保留下一位
                其他:设置%内解析标记为真
              }
          }
        是^:若%内解析标记为假,则转义下一个字符
        是":若%内解析标记为假,则转义字符直至下一个"
        是回车换行符(连续两个内容为0D0A):
          设置文件标记为0D0A之后
          执行代码
          中断本次循环,回到主循环
      }
    }
  }
}
作者: defanive    时间: 2011-8-23 03:41

本帖最后由 defanive 于 2011-8-23 04:54 编辑

在顶楼的帖子里面部分明确,但有不同意的地方

关于每次都重新读取批处理文件:
我认为批处理每执行一次就会重新读取一次批处理文件,如下例
先将批处理内容写为
@echo off
pause
运行批处理,在暂停的时候将批处理改为
@echo off
pause
pause
回到CMD按任意键后可以发现出现了按任意键继续的提示,说明CMD重新读取了一次批处理文件

关于不是分行读取批处理:
平常我们都认为CMD是一行一行读取批处理后分别处理并执行的
我认为CMD是从文件标记处开始一直读取,找到下一个合法的换行符之后设置文件标记并执行
下例说明了CMD不是分行读取并分行执行的
@echo off
echo a^
b
pause
此处充分说明了不是分行读取的,因为若是这样的话^号没有转义目标直接显示出a
下例说明CMD是以设置文件标记的方式往下运行的,先将批处理内容写为
@echo off
pause
echo 123
pause
然后运行批处理,在暂停的时候将批处理改为
echo off
pause
echo 123
pause
然后回到CMD并按继续,可以发现提示'cho'不是内部或外部命令
此例充分说明了CMD是以设置标记的方式读取的
运行到第一次pause时设置的标记是在文件的第18字节处(即是echo 123的开始),但是我们在运行时将@去掉使得文件往前移动了1字节,即此时第18字节对应的是cho 123,必然出现错误信息

关于边预处理边找新行、三个符号%^"实际是同时预处理:
平常认为是CMD找到了分行之后再进行预处理,我认为预处理与找新行是同时进行的
@echo off
set "a=^"
echo a%a%
b
pause
上例说明了预处理与找新行是同时的
因为如果是找到新行再进行预处理,则必然会显示a和'b'不是内部和外部命令
而事实是在找新行之前已经把%a%替换成了^号,并且^号把下面的回车换行符转义了,因此会继续寻找到pause之前,显示出ab
同时这也说明了%、^、"这三个符号是同时进行预处理,而不是像顶楼帖子所说的“先预处理完%,再进行^与"的并行预处理”
因为如果是先处理完%的话,此时^号并没有预处理,则回车换行符没有被转义,那么仍然应该显示a和'b'不是内部和外部命令
但事实是显示了ab,说明在替换完%之后,则同时继续进行了^和"的预处理
顶楼的帖子说明了这三者的优先级顺序,%总之处于最高优先级,"和^处于并列优先级
作者: defanive    时间: 2011-8-23 03:42

本帖最后由 defanive 于 2011-8-23 05:00 编辑

()的分组对预处理的影响
:标签的处理优先级
命令行参数去掉%0的规则
setlocal enableextensions会开启新的变量块,但是是否每次调用就开启一次新的
for的循环变量%%x处理问题
句柄操作的解释优先级
作者: defanive    时间: 2011-8-23 05:13

得到这些结果实在太震撼了…
颠覆了固有的思想,CMD竟然边预处理边找新行
我感觉继续往下挖掘,CMD会有更多更奇怪的事情被我们发现
作者: CrLf    时间: 2011-8-23 08:33

楼主的说法问题很大啊:
1、批处理不是事先读取整个文本,而是逐行解释、逐条执行
2、对 % 的预处理高于一切,它和 "、^ 完全不在一个重量级上

以下转自别处【】内是我补充的:
  1. cmd 执行一个批处理中的语句时是按照这样的流程进行的:
  2. 1、cmd 调用 shell 解释参数变量【%0~%9】、同时对 %str% 这种普通的变量形式进行解释【同时将单独的 % 作为自身的转义符看待,这三者是平级的,具体怎么预处理要看出现的次序】
  3. 2、切分语块、操作【语块的】句柄(如果存在针对语块的句柄的话)
  4. 3、划分语句,同时将参数分隔符转换为空格,并把有效的重定向(句柄操作)后置
  5. 4、当一个语句中存在使用了变量延迟的变量 !str! 时,将会被进行一次额外的针对转义符 ^ 和 !str! 的预处理
  6. 5、操作【单条命令的】句柄
  7. 6、执行语句
  8. 7、释放句柄
复制代码

作者: cjiabing    时间: 2011-8-23 09:51

回复 6# CrLf


    也不单单是单行或者整篇的预处理,而是分行兼分段式的预处理。那种简单的dos命令排列类的批处理脚本就是单行式的,而那种For、if、标签、call等则是成群处理。为何以行为目的?这个可能是cmd对dos继承的结果。最简单的试验就是,弄一篇代码,前面的代码是正确的,后面的代码是错误的,批处理的运行并未受到后面代码的影响。或者直接用“set”查看环境变量,只看到了命令执行过的环境变量,那些没有执行的命令及变量则没有遇到任何问题。如果弄一篇几十或者几百M的代码,用记事本打开编辑往往会产生很短时间的停顿,那么,运行开始的时候不知道是否会产生迟钝的现象?
    当然,这些不足以说明批处理没有做整篇式的预处理,只是从一个侧面反映了这种可能。
    我们设身处地地想,当年程序员开发批处理的时候,将批处理定位一种基础脚本,而脚本有可能成千上万行,如果都同时做预处理,可能导致浪费空间和时间,因为有些子程序可能从来不被执行过。标签存在的意义就是为了标记代码的位置,提供一种快速findstr相关代码。同样,在没有标签的长代码中,批处理默认为从上到下的运行,批处理在处理的时候也不必先将所有代码预处理完才执行。有可能如LZ所说的,边找代码边预处理边执行,但这个预处理不局限在命令执行所在的当前行,而可能是存在一个提前三步走的机制,犹如双线程,一条线程执行代码,一条线程负责预处理代码,而处理是提前一行或者几行,犹如先锋队。
    批处理执行流程有一个从上而下的过程,但有标签后,这个流程就会被打乱,有可能从下到上,但本质上还是从上到下的。然后是在同一行里的代码执行流程,也有改变顺序和方向的可能,在同一命令内部的执行顺序等等。比如if exist aa.txt (if exist bb.txt (if exist cc.txt (echo ok)  else (echo nice)) else (echo good))) else (echo not good)
——废话真多,狗屁不通,写不下去了,哈哈。
作者: defanive    时间: 2011-8-23 10:12

回复 6# CrLf
3L已经写了部分我认为是这样的依据
毕竟说话不能凭空讲,目前来说3L出现的预处理执行情况都能用2L解释
而且如果CMD是一行一行读取,^就没法对换行进行转义
而如果不是边预处理边执行,变量中的^也不能对换行转义
具体3L的例子应该能支持我的想法
作者: CrLf    时间: 2011-8-23 12:46

本帖最后由 CrLf 于 2011-8-23 13:00 编辑

回复 8# defanive


    对换行的转义至今没有看到很合理的解释,已有的都只是猜测(说实话我感觉 zqz 当时提出的观点比较含糊也比较牵强,可是至今确实没找到更合适的猜测),但是有一点我可能说得比较含糊,什么叫逐行解释?cmd 收到换行符时才认为结束了一行,但是当这一行为语块的一部分时(for 和 if 所代表的复合语句也是语块的一种),cmd 会继续对下一行进行处理,直到所处理的内容形成一个封闭的符合语法的语块。而 for 和 call 的根本区别之一就是 for 形成了自封闭的语块,而 call 不是。
cmd 绝不是同时对整个文本进行预处理的(for /f 除外),就算有,这又有什么意义呢?如果不同意这一观点,能否举个反例呢?

顺便说一下对 3 楼观点的一些个人看法:
  1. @echo off
  2. echo a^
  3. b
  4. pause
复制代码
  1. set 换行符=^
  2. cmd /v /c echo abc!换行符!123
  3. pause
复制代码
楼主似乎并没提出能够同时解释这两段代码的观点

而对于“预处理与找新行是同时的”我基本同意,但是这个“同时这也说明了%、^、"这三个符号是同时进行预处理”的说法却是错误的,顶楼的说法“先预处理完%,再进行^与"的并行预处理”反而是正确的...最简单的反例就是脚本中的 % 只能被 % 转义,而无法被 ^或者" 转义,这说明 % 和 ^、" 并不在同一个层面上工作
作者: defanive    时间: 2011-8-23 20:44

回复 9# CrLf
刚刚思考了一下的确%和^"不能同时进行,不然就要进行复杂的判断,因此2L有误
但是这样会有一个很严重的问题,继续回到这个批处理上
  1. @echo off
  2. set "a=^"
  3. echo a%a%
  4. b
  5. pause
复制代码
由执行结果可以很明显的看出,在找新行前,%已被拓展,^已完成对其他字符的转义
而%又要与^"分开的话,那么则CMD只能如下执行
对从标记开始的整个批处理文件完成%拓展 -> 转义^"的同时找新行
如果是这样的话,效率一定不如人意
作者: CrLf    时间: 2011-8-23 22:26

回复 10# defanive

我“感觉”“也许”是依照这样的流程:
  1.     cmd 从文件读取一行,每行以换行符 0A 结尾
  2.    预处理 %
  3.     预处理 ^ 和 ",再预处理 ( 与 )
  4.     若已读取的内容不完整则再读取下一行,直到形成封闭的语块
  5.    其他预处理步骤,此处暂且不提
复制代码
当然这种基于经验和过往思考的理解可能是有误的,仅作讨论之用,抛砖了,欢迎被雷到的同志们往死里砸砖,不过砸砖时请举反例或者提出更合理的猜测,我方可瞑目...
作者: defanive    时间: 2011-8-23 22:40

回复 11# CrLf
稍微纠正一下,换行是看0D0A而不是仅仅0A
我打算重新注入CMD看一下API调用的情况,到底是“加载->预处理->分行”还是“加载->分行->预处理->若行不完整继续加载”
作者: CrLf    时间: 2011-8-23 22:56

回复 12# defanive


能从根本上查看预处理方式的话,那是再好不过的了,期待

0D0A 是回车换行,而 cmd 确实是只以 0A 为断行依据的,这个早有定论,试一下就知道了。我曾怀疑 Windows 默认格式中,0A 之前的那个 0D 在 cmd 进行预处理时可能是被丢弃了,但是这个猜想似乎也不是很站得住脚:
虽然能解释
  1. echo a^
  2. b
  3. rem 0D 被丢弃,0A 被转义
复制代码
却依然无法解释
  1. set 换行符=^
  2. rem 这里非要三行才能获取换行符,这用我的猜测无法去解释。
复制代码
zqz 当时发现这个提取换行符的技巧时也提出了他的猜测,但是我个人感觉比较牵强,而且似乎也无法同时解释这两个现象,所以认为自己至今仍知其然而不知其所以然,只希望看到更合理的推测吧。
作者: defanive    时间: 2011-8-23 23:06

回复 13# CrLf
API HOOK技术也只能知道CMD调用API的情况,预处理这些仅仅是字符操作,没办法知道如何操作的。。
刚刚尝试了一下,的确是以0A为分割的,即批处理中只有0A而没有0D也能正常执行
但是如果按照^只是转义后面的字符来说的话,echo a^[换行]b这个根本就解释不通
作者: CrLf    时间: 2011-8-23 23:15

也是,当时没细想,看来那个的猜测似乎从一开始就是错的
作者: vsbat    时间: 2011-8-23 23:38

不怕苦不怕累。。。拿来OD,iDA ~~~Reverse
作者: defanive    时间: 2011-8-24 16:50

这些是刚刚注入CMD进行API HOOK获得的数据
下了钩子的API有CreateFileW(用于打开文件),SetFilePointer(用于设置文件指针),ReadFileW(用于读取文件内容)
被注入的批处理代码为
  1. InlineHook
  2. set "a=^"
  3. echo a%a%
  4. b
  5. pause
复制代码
运行的结果为
E:\Batch\InjectCMD>InlineHook
        //CreateFileW,          FileName:E:\Batch\InjectCMD\a.bat, Return:68
        //SetFilePointer,       Handle:68, Distance:0xc, Method:0, Return:0xc
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0xc
        //ReadFileW,            Handle:68, ToRead:8191
        //SetFilePointer,       Handle:68, Distance:0x17, Method:0, Return:0x17
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x17
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x17

E:\Batch\InjectCMD>set "a=^"
        //CreateFileW,          FileName:E:\Batch\InjectCMD\a.bat, Return:68
        //SetFilePointer,       Handle:68, Distance:0x17, Method:0, Return:0x17
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x17
        //ReadFileW,            Handle:68, ToRead:8191
        //SetFilePointer,       Handle:68, Distance:0x22, Method:0, Return:0x22
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x22
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x22
        //ReadFileW,            Handle:68, ToRead:8191
        //SetFilePointer,       Handle:68, Distance:0x25, Method:0, Return:0x25
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x25
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x25

E:\Batch\InjectCMD>echo ab
ab
        //CreateFileW,          FileName:E:\Batch\InjectCMD\a.bat, Return:68
        //SetFilePointer,       Handle:68, Distance:0x25, Method:0, Return:0x25
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x25
        //ReadFileW,            Handle:68, ToRead:8191
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x2a
        //ReadFileW,            Handle:68, ToRead:8191
        //SetFilePointer,       Handle:68, Distance:0x0, Method:2, Return:0x2a
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x2a

E:\Batch\InjectCMD>pause
请按任意键继续. . .     //CreateFileW,          FileName:CONIN$, Return:15


分析数据可以获得以下的事实(或者是猜想?):
1、每次执行一句批处理前都会重新打开文件
2、读取不是以行为单位读取的
3、每次读取的最大长度是8191(除去\0结尾)
4、可以看到echo a%a%[回车换行]b这一句的时候读取了两次批处理,说明CMD已经发现了回车换行符被转义
5、似乎CMD的执行过程与我们想象的都不同

希望大家继续分析这些结果,如果有需要这个注入程序的可以找我要
作者: defanive    时间: 2011-8-24 16:56

这一次注入的批处理内容为:
  1. InlineHook
  2. echo aaaa...(超过8192个a)
  3. pause
复制代码
得到的结果是
E:\Batch\InjectCMD>InlineHook
        //CreateFileW,          FileName:E:\Batch\InjectCMD\a.bat, Return:68
        //SetFilePointer,       Handle:68, Distance:0xc, Method:0, Return:0xc
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0xc
        //ReadFileW,            Handle:68, ToRead:8191
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x200b

        //ReadFileW,            Handle:68, ToRead:8191
        //SetFilePointer,       Handle:68, Distance:0x296b, Method:0, Return:0x2
96b
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x296b

        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x296b


E:\Batch\InjectCMD>echo aaaaaaaaaa...(很多a)
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa //CreateFileW,          FileName:E:\Batch\Inject
CMD\a.bat, Return:68
        //SetFilePointer,       Handle:68, Distance:0x296b, Method:0, Return:0x2
96b
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x296b

        //ReadFileW,            Handle:68, ToRead:8191
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x2970

        //ReadFileW,            Handle:68, ToRead:8191
        //SetFilePointer,       Handle:68, Distance:0x0, Method:2, Return:0x2970

        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x2970


E:\Batch\InjectCMD>pause
请按任意键继续. . .     //CreateFileW,          FileName:CONIN$, Return:15

作者: defanive    时间: 2011-8-24 17:05

这似乎说明,CMD必须找到一个回车换行符才会停止继续读取批处理
但是到底是“找到一个回车换行符 -> 预处理 -> 若被转义继续找”还是“读取8191 -> 预处理 -> 若没找到未被转义的回车换行符则继续读取8191”
作者: defanive    时间: 2011-8-24 17:31

这次注入的代码是获取换行符的代码
还没分析执行结果,大家帮忙看一下,说不定能明白CMD处理0D0A的方式
  1. InlineHook
  2. set CrLf=^
  3. pause
复制代码
执行结果
E:\Batch\InjectCMD>InlineHook
        //CreateFileW,          FileName:E:\Batch\InjectCMD\a.bat, Return:68
        //SetFilePointer,       Handle:68, Distance:0xc, Method:0, Return:0xc
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0xc
        //ReadFileW,            Handle:68, ToRead:8191
        //SetFilePointer,       Handle:68, Distance:0x18, Method:0, Return:0x18
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x18
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x18
        //ReadFileW,            Handle:68, ToRead:8191
        //SetFilePointer,       Handle:68, Distance:0x1a, Method:0, Return:0x1a
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x1a
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x1a
        //ReadFileW,            Handle:68, ToRead:8191
        //SetFilePointer,       Handle:68, Distance:0x1c, Method:0, Return:0x1c
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x1c
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x1c

E:\Batch\InjectCMD>set CrLf=

        //CreateFileW,          FileName:E:\Batch\InjectCMD\a.bat, Return:68
        //SetFilePointer,       Handle:68, Distance:0x1c, Method:0, Return:0x1c
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x1c
        //ReadFileW,            Handle:68, ToRead:8191
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x21
        //ReadFileW,            Handle:68, ToRead:8191
        //SetFilePointer,       Handle:68, Distance:0x0, Method:2, Return:0x21
        //SetFilePointer,       Handle:68, Distance:0x0, Method:1, Return:0x21

E:\Batch\InjectCMD>pause
请按任意键继续. . .     //CreateFileW,          FileName:CONIN$, Return:15





欢迎光临 批处理之家 (http://bbs.bathome.net/) Powered by Discuz! 7.2