OldLiu 发表于 2019-2-24 09:42:59

【老刘谈算法003】命令行参数的处理和获取——ArgCl函数实现分析

在非汇编语言中,处理并分割命令行参数(CmdLine)一般是由编译器在可执行文件中预置处理代码或者调用运行时库完成,
而在汇编中,我们需要手动调用Windows的API——GetCommandLine函数来获得一份杂乱的字符串(包含了可执行文件路径以及命令行参数),
还好在MASM32Library中,汇编爱好者们提供了一个取得命令行参数的函数,省去了我们亲自造轮子的功夫,
对于各种不按套路出牌的CmdLine,该函数将如何处理?这激起了我分析这段代码的兴趣。
当然,下面所列举的算法及出现特殊情况时的处置办法,可能与其他语言的处理方式不同,仅供参考。
算法流程如下(流程图绘制有bug,所以下面的一些文字可能有些怪异):
https://attach.52pojie.cn/forum/201808/23/202933rebzct13tte3ieub.png
代码如下:

;原出处:MASM32 SDK
;注释修改&中文注释添加 By 老刘

; #########################################################################
;
;            这个程序是在Iczelion和Lucifer的技术支持下开发出来的。
;
; #########################################################################

      .386
      .model flat, stdcall; 32 bit memory model
      option casemap :none; case sensitive

      include \MASM32\INCLUDE\kernel32.inc

; ------------------------------------
; 请读文本最后的用法
; ------------------------------------

    ArgCl PROTO :DWORD,:DWORD

    .code

; #########################################################################

ArgCl proc ArgNum:DWORD, ItemBuffer:DWORD

    LOCAL cmdLine      :DWORD
    LOCAL cmdBuffer :BYTE
    LOCAL tmpBuffer :BYTE

; --------------
; 储存 esi & edi
; --------------
    push esi
    push edi

    invoke GetCommandLine
    mov cmdLine, eax      ; address command line

    cmp ArgNum, 0
    jne @F
    xor eax, eax
      jmp jmp_In
    @@:

    mov esi, cmdLine
    lodsb                   ;al=byte ptr
    cmp al, 34
    je @F                   ;如果不是半角双引号,退出
      pop edi
      pop esi
      xor eax, eax          ; eax=0
      ret
    @@:
   
; -------------------------------------------------
; 统计引号标记来确认其是否双双匹配
; -------------------------------------------------
    xor ecx, ecx            ; 将ecx作为引号个数的计数器
    mov esi, cmdLine
   
    @@:
      lodsb
      cmp al, 0                                ;到达末尾
      je @F
      cmp al, 34            
      jne @B                                        ;不为引号则继续循环
      inc ecx                               ;ecx+=1
      jmp @B
    @@:

    push ecx                ;储存计数的值

    shr ecx, 1
    shl ecx, 1            ;ecx-=ecx%2

    pop eax               ;eax=计数的值
    cmp eax, ecx
    je @F                                        ;引号数量为偶数(匹配)则下跳
      pop edi
      pop esi
      mov eax, 3            ;返回3
      ret
    @@:

; -------------------------------------------
; 下面的代码移除(程序的)路径及文件名
; cmdline只留下参数
; -------------------------------------------
    mov esi, cmdLine                ;esi=cmdline指针
    lea edi, tmpBuffer                ;edi=tmpBuffer指针
   
    lodsb         ;读第一个引号

    @@:                                ;去除字符直到读到第二个引号
      lodsb
      cmp al, 34
      jne @B
      
    wtIn:
      lodsb
      cmp al, 0
      je wtOut
      stosb         ;转移到tmpBuffer
      jmp wtIn
    wtOut:
      stosb         ;tmpBuffer结尾

; -----------------------------------
; 处理空参数""(其实就是""->255)
; -----------------------------------

    lea esi, tmpBuffer
    lea edi, cmdBuffer

    xor edx, edx    ;清0,作为标志寄存器使用

    rnsSt:
      lodsb
      cmp al, 0       
      je rnsEnd                ;esi指向null,即处理完毕

      .if al != 34                        ;该字符不为引号
      .if edx == 1                ;前一字符为引号
          xor edx, edx        ;edx=0
          jmp rnsWrt
      .endif
      .elseif al == 34        ;该字符为引号
      .if edx == 1                ;前一字符为引号
          mov al, 255
          stosb                                ;edi指向地址=255,edi++,其实就是引号换255
          mov al, 34                ;再写入一个引号
          stosb
          dec edx                        ;edx=0
          jmp rnsSt
      .elseif edx == 0
          inc edx                        ;标记
      .endif
      .endif

    rnsWrt:
      stosb
      jmp rnsSt

    rnsEnd:
      stosb                ;cmdBuffer结尾

; ----------------------------------
; 替换有引号包裹的空格(引号中的空格->254)
; 删除引号(上面""换成的255不会被删除)
; ----------------------------------
    lea esi, cmdBuffer                ;下面操作的是同一个地址,但由于先读后写,并不冲突。
    lea edi, cmdBuffer

    subSt:
      lodsb
      cmp al, 0
      jne @F
      jmp subOut                ;完成
    @@:
      cmp al, 34
      jne subNxt
      jmp subSl                        ;如果有引号,进入子循环
    subNxt:
      stosb                                ;写回去
      jmp subSt
    ; --------------------------
    subSl:
      lodsb
      cmp al, 32    ;是空格
      jne @F
      mov al, 254 ;空格换254
      @@:
      cmp al, 34
      jne @F
      jmp subSt        ;子循环完毕,返回
      @@:
      stosb                                ;写回去
      jmp subSl
    ; --------------------------
    subOut:
      stosb         ;结尾

    ; ------------------------
    ;Tab-->空格
    ; ------------------------

    lea esi, cmdBuffer
    lea edi, cmdBuffer

    @@:
      lodsb
      cmp al, 0
      je rtOut
      cmp al, 9
      jne rtIn                        ;不为Tab跳
      mov al, 32                ;9-->32
    rtIn:
      stosb
      jmp @B
    rtOut:
      stosb

; ----------------------------------------------------
; 下面的代码分割正确的arg,
; 并且将arg写入目的地址
; ----------------------------------------------------
    lea eax, cmdBuffer
    mov esi, eax
    mov edi, ItemBuffer ; 目标地址

    mov ecx, 1          ; 做计数器

; ---------------------------
; 去掉先导空格(如果有的话)
; ---------------------------
    @@:
      lodsb
      cmp al, 32
      je @B

    l2St:
      cmp ecx, ArgNum   ; 传入的ArgNum
      je clSubLp2                                ;到达需要取参的位置
      lodsb
      cmp al, 0
      je cl2Out
      cmp al, 32
      jne cl2Ovr         ; if not space

    @@:
      lodsb
      cmp al, 32          ; 捕捉连续的空格
      je @B

      inc ecx             ;arg计数器++
      cmp al, 0
      je cl2Out

    cl2Ovr:
      jmp l2St

    clSubLp2:
      stosb                                ;参数第一个字符写进去
    @@:
      lodsb
      cmp al, 32
      je cl2Out
      cmp al, 0
      je cl2Out
      stosb
      jmp @B

    cl2Out:
      mov al, 0
      stosb

; ---------------------------------
; 空格换回去
; ---------------------------------
    mov esi, ItemBuffer
    mov edi, ItemBuffer

    @@:
      lodsb
      cmp al, 0
      je @F
      cmp al, 254
      jne nxt1
      mov al, 32
    nxt1:
      stosb
      jmp @B
    @@:

; -------------------------------------------------
; 替换“”为0
; -------------------------------------------------
    mov esi, ItemBuffer
    mov edi, ItemBuffer
    lodsb
    cmp al, 255 ;见108+行
    jne @F
    mov al, 0   ;替换为0
    stosb
    mov eax, 4;返回4
    pop edi
    pop esi
    ret
@@:

; ----------------------------------
; 如果参数数量不够,
; ItemBuffer首位设0
; ----------------------------------

    .if ecx < ArgNum
    jmp_In:
      mov edi, ItemBuffer
      mov al, 0
      stosb                        ;mov byte ptr ,al
      mov eax, 2;返回值为2说明参数数量不够或ArgNum为0
      jmp @F
    .endif

    mov eax, 1;cmdline成功处理

    @@:

      pop edi
      pop esi

    ret

ArgCl endp

; #########################################################################
;
;      这个子程序接收2个参数
;
;       1.你要获取的命令行参数的序号。
;       2.结果的缓存区地址。
;      
;       Example: 如果有4个参数放置于CmdLine中,
;       如progname arg1 arg2 arg3 arg4 , 使用如下手段调用该函数,
;
;       invoke ArgCl,3,ADDR buffer
;
;       将会将arg3放入buffer。
;
;       注意,引用的Arg支持长文件名。
;
;       确保缓存区的大小足够接收参数,
;       cmdline最大只能有128字节长(现在为32kb(非CMD中传参),该代码的编写日期为1999年),
;       在一个函数中,使用
;
;       LOCAL Buffer:BYTE
;
;       eax中的返回值
;
;       0 = GetCommandLine返回Null
;       1 = cmdline成功处理
;       2 = 已经尝试处理,但是参数数量不够或ArgNum为0
;       3 = 引号数目不匹配(不为偶数)
;       4 = 空的引号标记(类似"")
;
;       Tab和空格都为界定字符
;
; #########################################################################

end
## 漏洞分析 ##
当有效参数长度>128时,或当PE文件路径后面的参数长>192时,会发生溢出bug。
当传入参数的字符串内部出现半角双引号时(如        arg""arg),会触发"空引号标记"未换回BUG,原因为Line280~Line291作者只判断了第一个字符是否被标记为"空引号标记"。

作者的脑洞还是不够大啊(笑)
页: [1]
查看完整版本: 【老刘谈算法003】命令行参数的处理和获取——ArgCl函数实现分析