【源码分析】UPX 源码分析

main.cpp

int __acc_cdecl_main main(int argc, char *argv[])

1416 - 1426 行

    int i;
    static char default_argv0[] = "upx";
//    int cmdline_cmd = CMD_NONE;

#if 0 && (ACC_OS_DOS32) && defined(__DJGPP__)
    // LFN=n may cause problems with 2.03's _rename and mkdir under WinME
    putenv("LFN=y");
#endif
#if (ACC_OS_WIN32 || ACC_OS_WIN64) && (ACC_CC_MSC) && defined(_WRITE_ABORT_MSG) && defined(_CALL_REPORTFAULT)
    _set_abort_behavior(_WRITE_ABORT_MSG, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
#endif

这部分在 Linux 下没有启用,是 Windows 的东西,先不看了

1427 - 1434 行

    acc_wildargv(&argc, &argv);

    upx_sanity_check();
    opt->reset();

    if (!argv[0] || !argv[0][0])
        argv[0] = default_argv0;
    argv0 = argv[0];
  • acc_wildargv(&argc, &argv) 这个东西内部也没在 Linux 启用,不做研究

  • upx_sanity_check() 主要是对 UPX 程序信息的完整性检查,以及信息的复制

    • UPX_VERSION_STRING4:UPX 版本号长度检查,后面的 4 代表长度为 4
    • UPX_VERSION_YEAR:UPX 源码编写的年份,也是 4 位数
  • opt->reset():UPX 执行时的配置,该配置信息是个结构体,其定义源码在 src/options.h

  • if (!argv[0] || !argv[0][0]):这边检查的是有没有 argv[0] 以及 argv[0] 里面有没有内容。

    如果没有内容的话,那么就设置 argv[0] 的值为 upx

1435 - 1457 行

#if (ACC_OS_CYGWIN || ACC_OS_DOS16 || ACC_OS_DOS32 || ACC_OS_EMX || ACC_OS_TOS || ACC_OS_WIN16 || ACC_OS_WIN32 || ACC_OS_WIN64)
    {
        char *prog = fn_basename(argv0);
        char *p;
        bool allupper = true;
        for (p = prog; *p; p++)
            if (islower((unsigned char)*p))
                allupper = false;
        if (allupper)
            fn_strlwr(prog);
        if (p - prog > 4)
        {
            p -= 4;
            if (fn_strcmp(p, ".exe") == 0 || fn_strcmp(p, ".ttp") == 0)
                *p = 0;
        }
        progname = prog;
    }
#else
    progname = fn_basename(argv0);
#endif
    while (progname[0] == '.' && progname[1] == '/'  && progname[2])
        progname += 2;

这里直接到了 else 分支,也就是说生效的语句只有 progname = fn_basename(argv0);

这里的作用是获取 argv[0] 上的 basename,拿 Python 的形式举个例子就是:

fn_basename("/root/123/123/upx") == "upx"

下面的 while 语句在 Linux 上应该用不到,可能是用来过滤 Windows 上的路径的

1459 - 1477 行

    set_term(stderr);

    if (upx_ucl_init() != 0)
    {
        show_head();
        fprintf(stderr,"ucl_init() failed - check your UCL installation !\n");
        if (UCL_VERSION != ucl_version())
            fprintf(stderr,"library version conflict (%lx, %lx) - check your UCL installation !\n",
                    (long) UCL_VERSION, (long) ucl_version());
        e_exit(EXIT_INIT);
    }
    assert(upx_lzma_init() == 0);
    assert(upx_zlib_init() == 0);
#if (WITH_NRV)
    assert(upx_nrv_init() == 0);
#endif

    //srand((int) time(NULL));
    srand((int) clock());
  • set_term(stderr):该函数是用于定义全局变量 (FILE *)con_term

    • 如果参数不为空,则设置 con_term 为传入的参数代表的文件描述符地址
    • 如果参数为空,即参数为 0,那么会判断 0 号 fd 是否为终端设备(stdin、stdout、stderr)
      • 如果 0 号设备是终端设备,那么使 con_termstderr,即 *con_term = _IO_2_1_stderr_
      • 如果 0 号设备不是终端设备,那么使 con_termstdout
  • upx_ucl_init():如果 UCL 模块能成功初始化,那么返回 0,如果失败的话就进入这个括号输出报错信息

  • 后面的一堆 xxxinit 函数内部都仅仅是返回 0 而已,没有执行别的什么

  • 最后开启动态伪随机化

1479 - 1521 行 < upx 功能区 >

    /* get options */
    first_options(argc,argv);
#if defined(OPTIONS_VAR)
    if (!opt->no_env)
        get_envoptions(argc,argv);
#endif
    i = get_options(argc,argv);
    assert(i <= argc);

    set_term(NULL);
//    cmdline_cmd = opt->cmd;
    switch (opt->cmd)
    {
    case CMD_NONE:
        /* default - compress */
        set_cmd(CMD_COMPRESS);
        break;
    case CMD_COMPRESS:
        break;
    case CMD_DECOMPRESS:
        break;
    case CMD_TEST:
        break;
    case CMD_LIST:
        break;
    case CMD_FILEINFO:
        break;
    case CMD_LICENSE:
        show_license();
        e_exit(EXIT_OK);
        break;
    case CMD_HELP:
        show_help(1);
        e_exit(EXIT_OK);
        break;
    case CMD_VERSION:
        show_version(1);
        e_exit(EXIT_OK);
        break;
    default:
        /* ??? */
        break;
    }
  • first_options(argc,argv); 负责处理四种命令参数,分别是

    • --

    • --version

    • --help

    • --no-env

  • get_envoptions(argc,argv); 其内部执行了 getenv("UPX");

  • get_options(argc,argv); 相当于 getopt

    • 其内部参数有 123456789hH?VdLltfio:qvDk
  • set_term(NULL); 设置 con_term = stderr

switch (opt->cmd) 选择语句

选项 数值 命令行参数 用途
CMD_NONE 0 将 cmd 设置为 CMD_COMPRESS,跳转过去执行 upx 加壳
CMD_COMPRESS 1 -d 执行 upx 加壳
CMD_DECOMPRESS 2 <待补充> 执行 upx 脱壳
CMD_TEST 3 <待补充>
CMD_LIST 4 <待补充>
CMD_FILEINFO 5 <待补充>
CMD_LICENSE 7 <待补充>
CMD_HELP 6 <待补充> 显示帮助菜单
CMD_VERSION 8 <待补充> 显示 upx 版本
default 未知 预留位,如果有 bug 的话就从这出去了

1523 - 1535 行

    /* check options */
    if (argc == 1)
        e_help();
    set_term(stderr);
    check_options(i,argc);
    num_files = argc - i;
    if (num_files < 1)
    {
        if (opt->verbose >= 2)
            e_help();
        else
            e_usage();
    }
  • 如果没有参数的话,就显示 upx 的帮助菜单
  • 设置 con_term = stderr
  • check_options(i,argc); 与加壳脱壳关系不大,先不看了,主要是得到作为命令行参数的参数有几个,赋值给 i 变量
  • if 语句判断是是 总参数数量 - 作为参数的命令行参数数量,所以判断的就是有没有作为输入的文件
    • 如果没有就判断是要显示帮助菜单还是显示使用说明
    • 如果有的话就进入下面 upx 的执行区 的代码

1537 - 1561 行 < upx 执行区 >

    /* start work */
    set_term(stdout);
    do_files(i,argc,argv);

    if (gitrev[0])
    {
        bool warn = true;
        const char *ee = getenv("UPX_DISABLE_GITREV_WARNING");
        if (ee && ee[0] && strcmp(ee, "1") == 0)
            warn = false;
        if (warn)
        {
            FILE *f = stdout;
            int fg = con_fg(f, FG_RED);
            con_fprintf(f, "\nWARNING: this is an unstable beta version - use for testing only! Really.\n");
            fg = con_fg(f, fg);
            UNUSED(fg);
        }
    }

#if 0 && defined(__GLIBC__)
    //malloc_stats();
#endif
    return exit_code;
}
  • set_term(stdout); 设置 con_term = stdout
  • do_files(i,argc,argv); 执行 upx 程序对应参数的功能

work.cpp

void do_files(int i, int argc, char *argv[])

258 - 261 行

void do_files(int i, int argc, char *argv[]) {
    if (opt->verbose >= 1) {
        show_head();
        UiPacker::uiHeader();
    }

正常情况下 opt-verbose 的值为 2

  • show_head(); 的作用是打印 upx 执行时显示的头部信息(随版本改动),这些信息如下方所示:

                           Ultimate Packer for eXecutables
                              Copyright (C) 1996 - 2020
    UPX 3.96        Markus Oberhumer, Laszlo Molnar & John Reiser   Jan 23rd 2020
  • UiPacker::uiHeader(); 的作用是打印 upx 执行时的文件信息,这些信息如下所示:

            File size         Ratio      Format      Name
       --------------------   ------   -----------   -----------

263 - 305 行 < 待补充 >

    for (; i < argc; i++) {
        infoHeader();

        const char *iname = argv[i];
        char oname[ACC_FN_PATH_MAX + 1];
        oname[0] = 0;

        try {
            do_one_file(iname, oname);
        } catch (const Exception &e) {
            unlink_ofile(oname);
            if (opt->verbose >= 1 || (opt->verbose >= 0 && !e.isWarning()))
                printErr(iname, &e);
            set_exit_code(e.isWarning() ? EXIT_WARN : EXIT_ERROR);
        } catch (const Error &e) {
            unlink_ofile(oname);
            printErr(iname, &e);
            e_exit(EXIT_ERROR);
        } catch (std::bad_alloc *e) {
            unlink_ofile(oname);
            printErr(iname, "out of memory");
            UNUSED(e);
            // delete e;
            e_exit(EXIT_ERROR);
        } catch (const std::bad_alloc &) {
            unlink_ofile(oname);
            printErr(iname, "out of memory");
            e_exit(EXIT_ERROR);
        } catch (std::exception *e) {
            unlink_ofile(oname);
            printUnhandledException(iname, e);
            // delete e;
            e_exit(EXIT_ERROR);
        } catch (const std::exception &e) {
            unlink_ofile(oname);
            printUnhandledException(iname, &e);
            e_exit(EXIT_ERROR);
        } catch (...) {
            unlink_ofile(oname);
            printUnhandledException(iname, NULL);
            e_exit(EXIT_ERROR);
        }
    }

一开始 i 代表的是有无程序,有的话 i == 1否则 i == 0,没文件早退出了),然后遍历 argc 次

  • infoHeader(); 的作用是设置全局变量,代码为 info_header = 0;
  • 之后用 iname 获取从 argv[1] 开始的每一个参数,给 oname 数组开辟 0x400 的空间

void do_one_file(const char *iname, char *oname)

59 - 73 行

void do_one_file(const char *iname, char *oname) {
    int r;
    struct stat st;
    memset(&st, 0, sizeof(st));
#if (HAVE_LSTAT)
    r = lstat(iname, &st);
#else
    r = stat(iname, &st);
#endif
    if (r != 0) {
        if (errno == ENOENT)
            throw FileNotFoundException(iname, errno);
        else
            throwIOException(iname, errno);
    }
  • stat 结构体用来存取文件或者文件夹的信息,该结构体大小为 0x90

    struct stat {
        dev_t     st_dev;         /* ID of device containing file - 文件所在的设备 ID */
        ino_t     st_ino;         /* Inode number - inode 节点号 */
        mode_t    st_mode;        /* File type and mode - 保护模式 */
        nlink_t   st_nlink;       /* Number of hard links - 链向此文件的连接数(硬连接) */
        uid_t     st_uid;         /* User ID of owner - user id */
        gid_t     st_gid;         /* Group ID of owner - group id */
        dev_t     st_rdev;        /* Device ID (if special file) - 设备号,针对设备文件 */
        off_t     st_size;        /* Total size, in bytes - 文件大小,字节为单位 */
        blksize_t st_blksize;     /* Block size for filesystem I/O - 系统块的大小 */
        blkcnt_t  st_blocks;      /* Number of 512B blocks allocated - 文件所占块数 */
    
        /* Since Linux 2.6, the kernel supports nanosecond
           precision for the following timestamp fields.
           For the details before Linux 2.6, see NOTES. */
    
        struct timespec st_atim;  /* Time of last access - 最近存取时间 */
        struct timespec st_mtim;  /* Time of last modification - 最近修改时间 */
        struct timespec st_ctim;  /* Time of last status change - 最近状态改变时间 */
    
    #define st_atime st_atim.tv_sec      /* Backward compatibility */
    #define st_mtime st_mtim.tv_sec
    #define st_ctime st_ctim.tv_sec
    };
  • 因为正常情况下 HAVE_LSTAT == 1,所以这里执行的是 lstat,它能处理链接文件(这是 stat 所不能的)

  • lstat 或 stat 执行成功的话返回值为 0,那么返回值不为 0 的话就报 FileNotFoundException 的错误

74 - 97 行

    if (!(S_ISREG(st.st_mode)))
        throwIOException("not a regular file -- skipped");
#if defined(__unix__)
    // no special bits may be set
    if ((st.st_mode & (S_ISUID | S_ISGID | S_ISVTX)) != 0)
        throwIOException("file has special permissions -- skipped");
#endif
    if (st.st_size <= 0)
        throwIOException("empty file -- skipped");
    if (st.st_size < 512)
        throwIOException("file is too small -- skipped");
    if (!mem_size_valid_bytes(st.st_size))
        throwIOException("file is too large -- skipped");
    if ((st.st_mode & S_IWUSR) == 0) {
        bool skip = true;
        if (opt->output_name)
            skip = false;
        else if (opt->to_stdout)
            skip = false;
        else if (opt->backup)
            skip = false;
        if (skip)
            throwIOException("file is write protected -- skipped");
    }
  • S_ISREG(st.st_mode) 检查文件是否为常规文件

  • (st.st_mode & (S_ISUID | S_ISGID | S_ISVTX)) 避免对 s/S 权限的程序加壳

  • st.st_size 检查是否为空且程序是否小于 512 位(UPX 不支持对小于 0x40 B 的程序进行加壳)

  • mem_size_valid_bytes 检查程序是否大于 805306368 位(UPX 不支持对大于 0x60 MB 的程序进行加壳)

  • (st.st_mode & S_IWUSR) 判断用户是否有写权限,若是没有写权限

    • 如果是 UPX 读取的目标文件的话,报错

    • 如果是 UPX 输出的文件的话,不报错

99 - 110 行 < upx 读入程序 >

    InputFile fi;
    fi.st = st;
    fi.sopen(iname, O_RDONLY | O_BINARY, SH_DENYWR);

#if (USE_FTIME)
    struct ftime fi_ftime;
    memset(&fi_ftime, 0, sizeof(fi_ftime));
    if (opt->preserve_timestamp) {
        if (getftime(fi.getFd(), &fi_ftime) != 0)
            throwIOException("cannot determine file timestamp");
    }
#endif

这里要先列出来 UPX 自定义的文件基类

FileBase::FileBase() :
    _fd(-1), _flags(0), _shflags(0), _mode(0), _name(NULL), _offset(0), _length(0)
{
    memset(&st,0,sizeof(st));
}

以及刚初始化后的内存如下

pwndbg> p fi
$5 = {
  <FileBase> = {
    <File> = {
      _vptr.File = 0x5555557705c0 <vtable for InputFile+16>
    }, 
    members of FileBase:
    _fd = -1,
    _flags = 0,
    _shflags = 0,
    _mode = 0,
    _name = 0x0,
    _offset = 0,
    _length = 0,
    st = {
      st_dev = 0,
      st_ino = 0,
      st_nlink = 0,
      st_mode = 0,
      st_uid = 0,
      st_gid = 0,
      __pad0 = 0,
      st_rdev = 0,
      st_size = 0,
      st_blksize = 0,
      st_blocks = 0,
      st_atim = {
        tv_sec = 0,
        tv_nsec = 0
      },
      st_mtim = {
        tv_sec = 0,
        tv_nsec = 0
      },
      st_ctim = {
        tv_sec = 0,
        tv_nsec = 0
      },
      __glibc_reserved = {0, 0, 0}
    }
  }, 
  members of InputFile:
  _length_orig = 712
}
点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注