gnu-netcat执行部分原理分析(1)

5 分钟阅读时长

发布时间:

前言

最近VY-netcat遭遇开发瓶颈, 还是打算回去看看其他的程序是怎么实现这些功能的, 其中指令执行需要参考的是gnu-netcat.

有一说一好久没啃大项目了, 最近看着感觉还挺麻烦的, 还好gnu-netcat编写非常规范, 从0开始也能学到很多知识, 还是挺推荐开发者读一读源码, 附上源码地址: The GNU netcat - Browse /netcat/0.7.1 at SourceForge.net

分析

文件结构

项目结构就不详细介绍了, 很明显代码主要来自于./src文件夹下, 这里可以简单介绍一下VY-netcat的文件结构:

  • VY-netcat

    • bin: 存放编译文件路径

    • LICENSE: VY开源许可证

    • makefile: 用于linux/mac系统进行make的脚本, 想要理解编译原理可以从这个文件入手

    • README.md: markdown格式说明书

    • test: 用于存放测试文件的文件夹

      • execl: 用于测试c语言中的execl指令.

    • image: 图片文件夹, 用于存放相关图片

    • make.bat: 用于在windows上编译文件的执行脚本

    • src: 用于存放主要源码

      • client: 用于存放socket连接的函数模组

      • cmd: 用于存放终端交互的参数模组

      • log: 用于编写日志

      • netcat.v: 主要编译文件

      • netcat.c: 使用netcat.v进行编译的中间文件, 可使用gcc直接编译为二进制文件

VY-netcat在一些编写上参考了gnu-netcat, 可以此作为基础进行学习, 本次要分析-e, --exec=PROGRAM program to exec after connect参数的原理, 所以直接查看./src/netcat.c文件.

设置参数

直接定位到-e部分, 源码写得很清晰, 找到./src/netcat.cmain()函数:

/* 如果根本没有给出 args,则从 stdin 中获取它们并生成 argv */
/* Cmd line: */
if (argc == 1)
    netcat_commandline_read(&argc, &argv);

  /* 检查命令行开关 */
while (TRUE) {
    int option_index = 0;
    static const struct option long_options[] = {
    { "close",    no_argument,        NULL, 'c' },
    { "debug",    no_argument,        NULL, 'd' },
    { "exec",    required_argument,    NULL, 'e' },
    { "gateway",    required_argument,    NULL, 'g' },
    { "pointer",    required_argument,    NULL, 'G' },
    { "help",    no_argument,        NULL, 'h' },
    { "interval",    required_argument,    NULL, 'i' },
    { "listen",    no_argument,        NULL, 'l' },
    { "tunnel",    required_argument,    NULL, 'L' },
    { "dont-resolve", no_argument,        NULL, 'n' },
    { "output",    required_argument,    NULL, 'o' },
    { "local-port",    required_argument,    NULL, 'p' },
    { "tunnel-port", required_argument,    NULL, 'P' },
    { "randomize",    no_argument,        NULL, 'r' },
    { "source",    required_argument,    NULL, 's' },
    { "tunnel-source", required_argument,    NULL, 'S' },
#ifndef USE_OLD_COMPAT
    { "tcp",    no_argument,        NULL, 't' },
    { "telnet",    no_argument,        NULL, 'T' },
#else
    { "tcp",    no_argument,        NULL, 1 },
    { "telnet",    no_argument,        NULL, 't' },
#endif
    { "udp",    no_argument,        NULL, 'u' },
    { "verbose",    no_argument,        NULL, 'v' },
    { "version",    no_argument,        NULL, 'V' },
    { "hexdump",    no_argument,        NULL, 'x' },
    { "wait",    required_argument,    NULL, 'w' },
    { "zero",    no_argument,        NULL, 'z' },
    { 0, 0, 0, 0 }
    };

尝试直接运行nc, 出现Cmd line:以补充参数, 这里在VY-netcat v0.0.3版本更新情况中有解释过原因:

当windows用户直接打开程序时会因为无法传入参数导致闪退.

然后是下面定义gnu-netcat的参数, VY-netcat在设计时也有进行参考:

if args.len == 1 {
            mut data := args[0] + ' ' 
            data += read_line('Cmd line:') or { '' }
            args = data.split(' ')
    }

    long_options := [
        CmdOption{
            abbr: '-h'
            full: '--help'
            vari: ''
            defa: ''
            desc: 'display this help and exit.'
        }
        CmdOption{
            abbr: '-e'
            full: '--exec'
            vari: '[shell]'
            defa: 'false'
            desc: 'program to exec after connect.'
        }
        CmdOption{
            abbr: '-lp'
            full: '--listen_port'
            vari: '[int]'
            defa: 'false'
            desc: 'listen the local port number.'
        }
        CmdOption{
            abbr: '-klp'
            full: '--keep_listen_port'
            vari: '[int]'
            defa: 'false'
            desc: 'keep to listen the local port number.'
        }
    ]

控制

继续往下, gnu-netcat使用switch对参数进行控制:

c = getopt_long(argc, argv, "cde:g:G:hi:lL:no:p:P:rs:S:tTuvVxw:z",
            long_options, &option_index);
    if (c == -1)
      break;
    switch (c) {
        ...
        case 'e':            /* exec执行参数 */
        if (opt_exec)
        // 错误流 | 退出 | 报错日志(当e出现多次)
        ncprint(NCPRINT_ERROR | NCPRINT_EXIT,
            _("Cannot specify `-e' option double"));
        // 执行并返还.
        opt_exec = strdup(optarg);
        break;
        ...
    }

其中明显可以猜测ncprint()主要对nc过程中的日志进行打印, strdup()则是返回指向空终止字节字符串的指针, 故接下来可以选择调试或追踪opt_exec.

./netcat.c:61:char *opt_exec = NULL;            /*连接后执行程序*/
./netcat.c:122:  if ((p = strrchr(opt_exec, '/')))
./netcat.c:125:    p = opt_exec;
./netcat.c:129:  execl("/bin/sh", p, "-c", opt_exec, NULL);
./netcat.c:131:  execl(opt_exec, p, NULL);
./netcat.c:135:   opt_exec, strerror(errno));
./netcat.c:237:      if (opt_exec)
./netcat.c:242:      opt_exec = strdup(optarg);
./netcat.c:366:  if (opt_zero && opt_exec)
./netcat.c:504:      if (opt_exec) {
./netcat.c:604:      if (opt_exec) {

dup2

开始之前最好顺便简单了解一下execl上面的代码:

/* 将子进程进行重定向到socket */
dup2(ncsock->fd, STDIN_FILENO);    /* fiddlage 的精确顺序 */
close(ncsock->fd);            /* 显然是至关重要的;这是*/
dup2(STDIN_FILENO, STDOUT_FILENO);    /* swiped directly out of "inetd". */
dup2(STDIN_FILENO, STDERR_FILENO);    /* also duplicate the stderr channel */

通过解释, 将子进程重新定向到了socket, 也就是说在nc -e [执行代码]时我们实际新起了一个进程, 再将输入与输出通过socket发送到远程.

举个比较基本的重定向例子:

[root_cn@archlinux ~]$ neofetch
                   -`
                  .o+`
                 `ooo/
                `+oooo:
               `+oooooo:
               -+oooooo+:                root_cn@archlinux
             `/:-:++oooo+:               -----------------
            `/++++/+++++++:              OS: Arch Linux on Windows 10 x86_6 
           `/++++++++++++++:             Kernel: 4.4.0-19041-Microsoft
          `/+++ooooooooooooo/`           Uptime: 1 day, 6 hours
         ./ooosssso++osssssso+`          Packages: 602 (pacman)
        .oossssso-````/ossssss+`         Shell: bash 5.2.26
       -osssssso.      :ssssssso.        Terminal: Windows Terminal
      :osssssss/        osssso+++.       CPU: 11th Gen Intel i5-11300H (8)
     /ossssssss/        +ssssooo/-       Memory: 6593MiB / 16167MiB
   `/ossssso+/:-        -:/+osssso+-
  `+sso+:-`                 `.-/+oso:
 `++:.                           `-/+/
 .`                                 `/






[root_cn@archlinux ~]$ neofetch > a
[root_cn@archlinux ~]$ cat a
                   -`
                  .o+`
                 `ooo/
                `+oooo:
               `+oooooo:
               -+oooooo+:                root_cn@archlinux
             `/:-:++oooo+:               -----------------
            `/++++/+++++++:              OS: Arch Linux on Windows 10 x86_6 
           `/++++++++++++++:             Kernel: 4.4.0-19041-Microsoft
          `/+++ooooooooooooo/`           Uptime: 1 day, 6 hours
         ./ooosssso++osssssso+`          Packages: 602 (pacman)
        .oossssso-````/ossssss+`         Shell: bash 5.2.26
       -osssssso.      :ssssssso.        Terminal: Windows Terminal
      :osssssss/        osssso+++.       CPU: 11th Gen Intel i5-11300H (8)
     /ossssssss/        +ssssooo/-       Memory: 6593MiB / 16167MiB
   `/ossssso+/:-        -:/+osssso+-
  `+sso+:-`                 `.-/+oso:
 `++:.                           `-/+/
 .`                                 `/

execl执行

通过追踪opt_exec得到以上信息:

  • 第61行定义opt_exec
/* 执行一个外部文件,使其 stdin/stdout/stderr 成为实际的socket */

static void ncexec(nc_sock_t *ncsock)
{
  int saved_stderr;
  char *p;
  assert(ncsock && (ncsock->fd >= 0));

  /* 保存 stderr fd,因为我们以后可能需要它 */
  saved_stderr = dup(STDERR_FILENO);

  /* 将子进程进行重定向到socket */
  dup2(ncsock->fd, STDIN_FILENO);    /* fiddlage 的精确顺序 */
  close(ncsock->fd);            /* 显然是至关重要的;这是*/
  dup2(STDIN_FILENO, STDOUT_FILENO);    /* swiped directly out of "inetd". */
  dup2(STDIN_FILENO, STDERR_FILENO);    /* also duplicate the stderr channel */

  /*更改已执行程序的标签*/
  if ((p = strrchr(opt_exec, '/')))
    p++;            /*较短的argv[0]*/
  else
    p = opt_exec;

  /* 用新的过程替换此过程 */
#ifndef USE_OLD_COMPAT
  execl("/bin/sh", p, "-c", opt_exec, NULL);
#else
  execl(opt_exec, p, NULL);
#endif
  dup2(saved_stderr, STDERR_FILENO);
  ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Couldn't execute %s: %s"),
      opt_exec, strerror(errno));
}                /* 结束ncexec() */

static表示函数为静态函数.

静态函数的作用是实现与类相关的功能,而不需要创建实例。静态全局变量仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响.

execl语法为:

int execl(const char *path, const char *arg, ...);

可以看到p作为调用函数的argv[0]参数, 有两种执行方式:

  • /bin/sh下传参为p -c opt_exec NULL

  • opt_exec下传参为p NULL

示例

我们可以自己构造一些简单代码以了解execl函数:

编译hello.c文件

// clang hello.c -o hello
#include <stdio.h>
int main(int argc,char *argv[],char *envp[]){
        printf("Filename: %s\n",argv[0]);
        printf("%s %s\n",argv[1],argv[2]);
        return 0;
}

然后编译return.c文件:

// clang return.c -o return
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
        char *temp,*temp1,*temp2;
        temp="test";  //Filename
        temp1="Funny";
        temp2="world";

        execl("hello",temp,temp1,temp2,NULL);
        printf("Error");
        return 0;
}a
/* return:
 * Filename: test
 * Funny world
**/

可以发现本质上execl添加了一个新的进程进行工作(我觉得这么说很容易理解, 有问题欢迎指出).

分析结论

仅从一个静态函数ncexec()能分析到的信息有限, 不过我们从execl()方法大致可以思考gnu-netcat的执行原理: 从外部调用脚本进行执行, 然后重定向到当前连接的远程系统中.

之后还需要考虑重定向在vlang上的使用问题, 对于gnu-netcat上执行方式与linux原理也需要更进一步学习, 有时间的话继续写写.