返回

汇编实现捕获子进程输出:execve/fork/dup2/pipe详解

Linux

如何捕获由其他程序启动的程序的标准输出?

我有一个Linux下的二进制可执行文件,假设叫 myecho,它从命令行读取参数并将它们回显到标准输出。现在,我想用汇编语言编写另一个二进制可执行文件(不使用 libc 或其它 C 库),假设它的名字是 myprog。我希望 myprog 使用内核函数 sys_execve 启动 myecho,并想将 myecho 的标准输出捕获到文件 messages.txt 中以供进一步处理。

问题来了,我不知道如何让被启动的 myecho 将其输出重定向到文件。

myprog program
[.data]
Par1        DB "Param1",0
Par2        DB "Param2",0
Par3        DB ">messages.txt",0
Env1        DB 0 ; Not used.
Command     DB "/path/to/myecho",0
Parameters  DD Par1,Par2,Par3,0
Environment DD Env1
[.text]
; Execute "myecho" from "myprog":
MOV EAX,11          ; Kernel # of "execve" in 32bit Linux.
MOV EBX,Command     ;
MOV ECX,Parameters  ;
MOV EDX,Environment ;
INT 0x80

我原本以为 execve 执行的命令行类似于这样:

 /path/to/myecho Param1 Param2 >messages.txt

但结果并非如此。>messages.txt 被逐字地当作第三个参数。我试过用 ^ 或者 Esc 来转义重定向运算符 >

Par3    DB "^>messages.txt",0
Par3    DB 27,">messages.txt",0

但没啥用。也许我需要用 fork 而不是 execve 来启动 myecho,并使用 tee 来管道化它的输出,但我又不知道怎么把 myecho 的输出连接到 tee 的文件符上。

要怎么做才能在 myprog 中运行 myecho,把它的输出捕获到文件,然后在 myprog 中处理这个输出?

问题原因

直接使用 execve 并把重定向符号 > 作为参数传递是行不通的。因为 execve 不会启动 shell 来解释这个重定向。execve 会把 >messages.txt 当作一个普通参数直接传给 myechomyecho 自己不会处理这个重定向符号的。

重定向是由 shell(如 bash)完成的。当你在命令行里敲入 command > file 时,shell 会负责打开 file,并把 command 的标准输出重定向到这个文件。

解决方案

下面给出几种解决思路:

方案一: 使用 sh -c

一个简单的解决办法是,通过启动一个 shell, 让 shell 去执行命令并处理重定向.

  1. 原理: 我们可以调用 /bin/sh,并使用 -c 选项来执行一个完整的 shell 命令, 这条命令包含了我们要运行的程序和输出重定向。

  2. 代码示例 (汇编):

    myprog program
    [.data]
    ShCmd       DB "/bin/sh",0
    DashC       DB "-c",0
    FullCmd     DB "/path/to/myecho Param1 Param2 > messages.txt",0
    Env1        DB 0
    Parameters  DD ShCmd,DashC,FullCmd,0
    Environment DD Env1
    
    [.text]
    ; Execute "myecho" from "myprog" via shell:
    MOV EAX,11          ; sys_execve
    MOV EBX,ShCmd       ; "/bin/sh"
    MOV ECX,Parameters  ; Parameters (including -c and the full command)
    MOV EDX,Environment ; Environment (can be 0)
    INT 0x80
    
  3. 解释: 这次 execve 启动 /bin/sh/bin/sh-c 参数告诉它后面的字符串是一个要执行的完整的命令。 这个完整的命令,也就是 /path/to/myecho Param1 Param2 > messages.txt,由 shell 解析,> 会被 shell 正确地解释为输出重定向。

  4. 安全建议: 这个方法虽然简单,但如果 /path/to/myecho, Param1, 或 Param2 这些参数来源于用户输入,一定要小心处理,防止命令注入攻击. 如果 Param1 的内容是 "; rm -rf /; echo " , 那可就糟了!

方案二: forkexecvedup2 组合拳

更“底层”、更灵活的方法,不依赖 shell,直接操作文件符。

  1. 原理:

    • 先用 fork 创建一个子进程。
    • 在子进程中:
      • 打开目标文件(messages.txt)。
      • 使用 dup2 把标准输出(文件描述符 1)重定向到这个文件。
      • 使用 execve 执行 myecho
    • 在父进程中:
      • 使用 waitwaitpid 等待子进程结束.
      • (可选) 从 messages.txt 文件读取内容并处理。
  2. 代码示例(汇编,只展示子进程部分,需要自行实现 sys_open, sys_dup2, sys_closesys_forksys_waitpid):

    [.data]
    FileName  DB "messages.txt",0
    ... (其他数据,例如 myecho 的路径和参数) ...
    
    [.text]
    ; ... (fork 的代码,检查返回值以确定是父进程还是子进程) ...
    
    ; 子进程的代码:
    ; 打开文件:
        MOV EAX, 5       ; sys_open
        MOV EBX, FileName
        MOV ECX, 0x41     ; O_WRONLY | O_CREAT , 0x40 | 0x1
        MOV EDX, 0644o   ; 权限 (rw-r--r--)
        INT 0x80         ; 返回文件描述符到 EAX
        ; 如果打开文件出错... 处理错误...
        CMP EAX, 0
        JL  Error_File_Open
    
        MOV EBX, EAX          ;文件描述符
    
    ; 重定向 stdout:
        MOV EAX, 63       ; sys_dup2
        ;EBX 里放的是文件描述符, 上面放进去了.
        MOV ECX, 1      ; STDOUT_FILENO
        INT 0x80
       ; 如果出错... 处理错误...
    
    ; 现在,标准输出被重定向到了文件. 可以执行 myecho 了:
    
         ;关闭原文件描述符:
        MOV EAX, 6        ; sys_close.
        ;EBX:  文件描述符.  在 sys_dup2 和这里是同一个.
        INT 0x80
    
        MOV EAX, 11          ; sys_execve
        MOV EBX, ...      ; myecho 的路径
        MOV ECX, ...      ; myecho 的参数
        MOV EDX, ...      ; 环境变量 (可以为 0)
        INT 0x80
    
         ; 如果 execve 失败 (通常不会,除非 myecho 不存在)... 处理错误...
    
    Error_File_Open:
         ;文件打开出错的处理代码
         ; ...
    
  3. 解释: dup2(oldfd, newfd)newfd 重定向到 oldfd。 意思是, 所有本来要写入 newfd 的数据, 都被写到 oldfd 所指向的文件. 在这个例子中,oldfdmessages.txt 的文件描述符,newfd 是 1 (标准输出),所以所有原本应该输出到屏幕上的内容,都被写入了 messages.txt。关闭原来文件描述符是好习惯, 免得泄露.

  4. 安全建议: 这种方式需要自己管理文件描述符, 确保正确地打开和关闭文件,避免文件描述符泄露。 确保在 fork 之后, 子进程立即执行必要的重定向和 execve, 防止出现任何可能修改文件描述符的操作, 从而降低安全风险。

  5. 进阶使用技巧: 可以考虑创建管道 (使用 pipe 系统调用),而不是直接重定向到文件。这样 myprog 可以直接通过管道读取 myecho 的输出,而不用写入中间文件.

方案三 (针对进阶要求的拓展) : 使用管道 (pipe)

此方案在方案二基础上进一步改进,不使用中间文件,直接在父子进程间通过管道通信。

  1. 原理:

    • 创建一个管道,得到两个文件描述符:一个用于读取(pipefd[0]),一个用于写入(pipefd[1])。
    • fork 创建子进程。
    • 在子进程中:
      • 关闭管道的读取端 (pipefd[0])。
      • 使用 dup2 将标准输出重定向到管道的写入端 (pipefd[1])。
      • 使用 execve 运行 myecho
    • 在父进程中:
      • 关闭管道的写入端 (pipefd[1])。
      • 从管道的读取端 (pipefd[0]) 读取数据,这些数据就是 myecho 的输出。
      • (可选) 使用 waitpid 等待子进程结束。
  2. 代码示例(汇编,仅展示关键部分):

      [.data]
          PipeFD   DD 0,0     ; 存储管道的两个文件描述符
    
    [.text]
     ;...
    
     ;创建管道.
          MOV EAX, 42          ; sys_pipe
          MOV EBX, PipeFD  ;文件描述符数组.
          INT 0x80
        ;  如果管道创建出错, 要有处理错误的代码...
      ; fork. 下面的代码是简化过的, 略过了处理父进程代码的情况和 waitpid:
    
          MOV     EAX, 2       ;sys_fork
          INT     0x80
    
           ; ...
           ; 这里是子进程 (如果 fork() 的结果是 0) ...
    
           ; 关闭读取端.
            MOV   EAX,  6
            MOV   EBX,  [PipeFD] ;  读取端在 PipeFD + 0 的地址
            INT   0x80
    
        ;  把 stdout 重定向到写入端:
           MOV      EAX,63      ; sys_dup2
           MOV     EBX, [PipeFD+4]  ;  写入端是第二个文件描述符 (PipeFD + 4)
           MOV     ECX,1       ; 1 = stdout.
           INT     0x80
           ;...错误处理...
    
             ;现在关闭原来的文件描述符:
            MOV EAX, 6
             MOV EBX,[PipeFD+4]   ;写入端是第二个文件描述符 (PipeFD + 4)
             INT 0x80
        ; execve 运行 myecho ...
       ; ------------------  父进程:
            ;在父进程里 (如果 fork 返回一个大于 0 的值):
    
           ;关闭写入端:
              MOV   EAX,6        ; sys_close
              MOV    EBX,[PipeFD + 4]     ;  写入端是第二个文件描述符.
               INT  0x80
           ; 现在, 通过循环从 [PipeFD] 里读数据就行, 这是 myecho 的输出:
       Read_Loop:
           MOV EAX, 3           ; sys_read
           MOV EBX, [PipeFD]    ; Read from the pipe's read end
           MOV ECX, Buffer       ; 缓冲区
           MOV EDX, BufferSize ; 缓冲区长度
           INT 0x80
           ; CMP EAX, 0
           ;  ... 根据读取的字节数处理,  循环等...
     ;....
    
     ; ...
    
     ;Wait for child...
        MOV      EAX, 7       ; sys_waitpid, 或 sys_wait4.
       MOV     EBX,-1      ;任何子进程.
       MOV      ECX, Wstatus    ;指向用于保存子进程 status 的存储区.
       MOV      EDX, 0        ;没有其它选项.
       INT       0x80
    
        ; ...
    
  3. 解释: 这样就构建了父子进程间的单向通信通道,myecho 的所有标准输出都会被写入管道,然后父进程 myprog 从管道中读取。 waitpid 很重要。用 sys_wait4 可能更灵活, 允许指定资源收集选项。

  4. 安全建议: 与方案二相似,务必正确管理文件描述符,注意关闭不需要的端口。 还要小心处理管道的缓冲区大小,如果 myecho 的输出太多, 会导致死锁 (如果父进程没有及时读取, 子进程的管道写端会被阻塞).。

以上三种方法, 方法一简单但有安全风险 (命令注入), 方法二更可靠,方法三更加直接高效 (不用文件). 选择哪个, 取决于具体的需求和安全考虑。 使用汇编来直接操控这些,可以做到精细控制. 但千万要仔细测试,并且写好错误处理的代码,否则容易产生 bug!