赞
踩
这篇博客主要是一个终端的骚操作,没有什么知识点,都是一些经验之谈,表述上甚至可能有错误。
在Linux的程序里(其实Windows里也差不多)一般能键盘输入的,就能用重定向输入,但是,有办法让人不能轻易用重定向代替键盘输入。
最简单的,ssh这个工具本身不支持直接使用参数输入密码(当然可以通过sshpass之类的工具在参数输入密码),而且ssh虽然支持重定向输入,但是只能把命令重定向过去,而无法把密码重定向过去。
类似ssh这样强制只能键盘输入的程序怎么实现呢?
举个最简单的例子(先用shell写一个最简单的实现):
#!/bin/bash
ptsname="/dev/$(ps | awk "\$1==\"$$\"{print \$2}")"
exec 200<"$ptsname"
inputdata=""
read -u 200 inputdata
exec 200<&-
echo "you input: $inputdata"
运行一些看看效果:
这样就写好了一个最简单的强制键盘输入的程序了,在使用echo "fu** you"
重定向到这个shell脚本的时候,它并没有从标准输入读入,而是从终端读入,你可以看看ptsname变量的内容,就是当前终端的路径。
这个shell脚本很简单,但是作为经验之谈,我稍微解释一下各行命令的作用:
首先是ptsname="/dev/$(ps | awk "\$1==\"$$\"{print \$2}")"
我们先看看ps的输出:
可以看到,ps未使用任何参数的情况下可以获取到当前进程以及父进程的信息,在这张图片里,当前进程就是ps,父进程就是bash。而$$是代替当前进程号码,在这里也就是3158这个bash的进程号码(专业一点的说法是pid)。
有些老六会说
$$
是父进程的pid而不是当前进程的pid,其实不是,$$
就是当前进程的pid,因为这些老六是这样用的:
echo "$$"
然后他就会说:“欸,获取到的是bash的pid而不是echo的pid”
我会说:“你在写命令行的时候,$$
就被替换为当前进程的pid了,传入给echo命令的时候$$
就是bash的pid,你让echo怎么搞才能输出echo的pid嘛。”
然后重点关注3158 pts/0 00:00:00 bash这行,你看看第二个字符串,pts/0,这玩意就是当前进程使用的终端,具体可以通过ll /proc/$$/fd/
看到,其中0就是标准输入,1是标准输出,2是标准错误,可以看到都是从终端读入的。
你可能已经猜到了,当使用重定向时,0就不是指向/dev/pts/0了,可以写个简单的脚本看看:
#!/bin/bash
ls -l /proc/$$/fd/
脚本内容很简单,正所谓大道至简。但是这个用法揭示了重定向的实质。
一开始什么重定向都没用,直接调用脚本,看到fd很正常,都是终端。但是当使用管道重定向输入后,0就指向管道了(如果用的是文件重定向输入,0会指向那个文件,你可以试试)。用了管道重定向输出后,1就指向到cat的管道了,当使用了2>&1
表示把标准错误也重定向到标准输出后,2也指向到cat的管道了。
所以说,之所以能够用重定向代替键盘输入,是因为它把文件描述符指向了管道或文件了。所以如果要强行只能在键盘输入,那很简单,反过来通过打开表示终端的文件只读取键盘输入就可以了。
后面的exec 200<"$ptsname"
就是在当前进程以读的方式打开ptsname表示的tty终端并且设置描述符为200;后面的read -u 200 inputdata
就是从文件描述符为200的文件里读入一行数据到inputdata变量里,200正好就是tty的描述符,所以就是从终端读入了,而不是从标准输入读入;再后面的exec 200<&-
是关闭200文件描述符;最后就给用户输出刚刚键盘输入的内容。
必须有,哪里有压迫,哪里就有反抗。原理很简单,它不是要从键盘输入吗,好,我就模拟从键盘输入!要模拟键盘输入只需要模拟终端输入即可。那这个问题不是变得很简单了吗,直接用C创建一个终端,然后给发送内容即可。废话不多说,上代码(这个是一个没有任何判断输出逻辑的代码,如果想做出sshpass的效果,需要把子进程的输出也重定向到从终端,然后搞一个子线程来做判断逻辑):
#define _XOPEN_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <signal.h> #include <fcntl.h> // 参数为: // 实际上要执行的程序 参数1 参数2 参数3 ... int main(int argc, char *argv[]) { int master_fd = open("/dev/ptmx", O_RDWR | O_NOCTTY); // 创建虚拟终端主终端 char *slave_name; int slave; pid_t pid; char buf[65]; ssize_t n; if (master_fd == -1) { return 1; } if (grantpt(master_fd) == -1) { close(master_fd); return 1; } if (unlockpt(master_fd) == -1) { close(master_fd); return 1; } slave_name = ptsname(master_fd); // 获取从终端名字(也就是文件名/dev/pts/0之类的东西) if (slave_name == NULL) { close(master_fd); return 1; } pid = fork(); if (pid < 0) { close(master_fd); return 1; } if (pid == 0) { // 子进程,在这里执行从终端 close(master_fd); // 从终端里不需要主终端的句柄 if (setsid() == -1) { _exit(1); } slave = open(slave_name, O_RDWR); if (slave == -1) { _exit(1); } dup2(slave, STDIN_FILENO); execvp(argv[1], argv + 1); } // 下面父进程负责一直等待用户标准输入,并且把内容传给子进程 while ((n = read(STDIN_FILENO, buf, sizeof(buf) - 1)) > 0) { // 其实大小没必要-1,但是为了打日志不被坑加上 buf[n] = '\0'; // 其实没必要,因为后面write的时候会明确长度,但是这样可以方便出问题时打日志不会被坑 write(master_fd, buf, n); // 把标准输入获取到的东西给虚拟终端传过去 } write(master_fd, "\x04", 1); // 发送键盘按下ctrl+d的控制符号,为什么是4可以看ANSI编码,4代表EOT(传输结束) wait(NULL); // 给子进程收尸 close(master_fd); return 0; }
稍微解释一下,master_fd是主终端句柄,slave是从终端句柄。这两个终端有一个特点,就是它们两可以通过这两个句柄交互:主终端往master_fd写东西,从终端就可以从slave读到;从终端往slave写东西,主终端就能从master_fd读到。所以dup2(slave, STDIN_FILENO)是把从终端读到主终端写到master_fd的内容重定向到从终端的标准输入。而我的逻辑是主终端获取到标准输入的时候,就写到master_fd。这样就相当于把这个程序的标准输入重定向到从终端的标准输入了。
让我们试试威力(我把代码保存为pass.c):
一开始是直接调用脚本,键盘输入hello后输出;
第二次是用重定向想输入内容为xxx,但是失败了,还是要求键盘输入;
第三次恼羞成怒,用这个代码编译了一个模拟终端输入的程序,然后就能重定向了。
你可能已经发现了,这个代码不仅能解决我这个强制键盘输入脚本这个例子,通过不同的参数,可以解决不同的程序。比如ssh:
第一次是没有用pass的对照,一回车就提示要键盘输入密码了;
第二次是用了pass的效果。
没错,就是这么简单的C代码,就可以模拟在键盘输入,从而达到模拟给ssh输入密码的效果(红色划掉的地方就是密码),因为ssh做了判断,不能在它显示能够root@127.0.0.1’s password:之前就输入密码,我这个程序没有写判断逻辑(感兴趣的话你可以自己加上,多不了几行代码,我这只是个最简单的例子),所以我通过sleep来粗略地控制流程。因为ssh默认是有回显的,所以你能看到在它下面的输出里也包含了模拟输入的命令内容。
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。