汇编实现捕获子进程输出:execve/fork/dup2/pipe详解
2025-03-12 01:01:57
如何捕获由其他程序启动的程序的标准输出?
我有一个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
当作一个普通参数直接传给 myecho
,myecho
自己不会处理这个重定向符号的。
重定向是由 shell(如 bash)完成的。当你在命令行里敲入 command > file
时,shell 会负责打开 file
,并把 command
的标准输出重定向到这个文件。
解决方案
下面给出几种解决思路:
方案一: 使用 sh -c
一个简单的解决办法是,通过启动一个 shell, 让 shell 去执行命令并处理重定向.
-
原理: 我们可以调用
/bin/sh
,并使用-c
选项来执行一个完整的 shell 命令, 这条命令包含了我们要运行的程序和输出重定向。 -
代码示例 (汇编):
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
-
解释: 这次
execve
启动/bin/sh
。/bin/sh
的-c
参数告诉它后面的字符串是一个要执行的完整的命令。 这个完整的命令,也就是/path/to/myecho Param1 Param2 > messages.txt
,由 shell 解析,>
会被 shell 正确地解释为输出重定向。 -
安全建议: 这个方法虽然简单,但如果
/path/to/myecho
,Param1
, 或Param2
这些参数来源于用户输入,一定要小心处理,防止命令注入攻击. 如果Param1
的内容是"; rm -rf /; echo "
, 那可就糟了!
方案二: fork
、execve
、dup2
组合拳
更“底层”、更灵活的方法,不依赖 shell,直接操作文件符。
-
原理:
- 先用
fork
创建一个子进程。 - 在子进程中:
- 打开目标文件(
messages.txt
)。 - 使用
dup2
把标准输出(文件描述符 1)重定向到这个文件。 - 使用
execve
执行myecho
。
- 打开目标文件(
- 在父进程中:
- 使用
wait
或waitpid
等待子进程结束. - (可选) 从
messages.txt
文件读取内容并处理。
- 使用
- 先用
-
代码示例(汇编,只展示子进程部分,需要自行实现
sys_open
,sys_dup2
,sys_close
,sys_fork
,sys_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: ;文件打开出错的处理代码 ; ...
-
解释:
dup2(oldfd, newfd)
把newfd
重定向到oldfd
。 意思是, 所有本来要写入newfd
的数据, 都被写到oldfd
所指向的文件. 在这个例子中,oldfd
是messages.txt
的文件描述符,newfd
是 1 (标准输出),所以所有原本应该输出到屏幕上的内容,都被写入了messages.txt
。关闭原来文件描述符是好习惯, 免得泄露. -
安全建议: 这种方式需要自己管理文件描述符, 确保正确地打开和关闭文件,避免文件描述符泄露。 确保在
fork
之后, 子进程立即执行必要的重定向和execve
, 防止出现任何可能修改文件描述符的操作, 从而降低安全风险。 -
进阶使用技巧: 可以考虑创建管道 (使用
pipe
系统调用),而不是直接重定向到文件。这样myprog
可以直接通过管道读取myecho
的输出,而不用写入中间文件.
方案三 (针对进阶要求的拓展) : 使用管道 (pipe)
此方案在方案二基础上进一步改进,不使用中间文件,直接在父子进程间通过管道通信。
-
原理:
- 创建一个管道,得到两个文件描述符:一个用于读取(
pipefd[0]
),一个用于写入(pipefd[1]
)。 fork
创建子进程。- 在子进程中:
- 关闭管道的读取端 (
pipefd[0]
)。 - 使用
dup2
将标准输出重定向到管道的写入端 (pipefd[1]
)。 - 使用
execve
运行myecho
。
- 关闭管道的读取端 (
- 在父进程中:
- 关闭管道的写入端 (
pipefd[1]
)。 - 从管道的读取端 (
pipefd[0]
) 读取数据,这些数据就是myecho
的输出。 - (可选) 使用
waitpid
等待子进程结束。
- 关闭管道的写入端 (
- 创建一个管道,得到两个文件描述符:一个用于读取(
-
代码示例(汇编,仅展示关键部分):
[.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 ; ...
-
解释: 这样就构建了父子进程间的单向通信通道,
myecho
的所有标准输出都会被写入管道,然后父进程myprog
从管道中读取。waitpid
很重要。用sys_wait4
可能更灵活, 允许指定资源收集选项。 -
安全建议: 与方案二相似,务必正确管理文件描述符,注意关闭不需要的端口。 还要小心处理管道的缓冲区大小,如果
myecho
的输出太多, 会导致死锁 (如果父进程没有及时读取, 子进程的管道写端会被阻塞).。
以上三种方法, 方法一简单但有安全风险 (命令注入), 方法二更可靠,方法三更加直接高效 (不用文件). 选择哪个, 取决于具体的需求和安全考虑。 使用汇编来直接操控这些,可以做到精细控制. 但千万要仔细测试,并且写好错误处理的代码,否则容易产生 bug!