Nafiez

Interested in x86 Reverse Engineering and Vulnerability Research.

CVE-2018-16517 - Netwide Assembler (NASM) - NULL Pointer Dereference

18 Sep 2018 » security, ,

Special thanks to Fakhri (@d0lph1n98) for helping out with the analysis :)

Description

asm/labels.c in Netwide Assembler (NASM) is prone to NULL Pointer Dereference, which allows the attacker to cause a denial of service via a crafted file.

Proof-of-Concept

  1. Craft a ASM code echo "equ push rax" > poc
  2. Compile the ASM file with following parameter nasm -f elf poc

Looking at the source code:

typedef struct insn { /* an instruction itself */
    char            *label;                 /* the label defined, or NULL */
    int             prefixes[MAXPREFIX];    /* instruction prefixes, if any */
    enum opcode     opcode;                 /* the opcode - not just the string */
    enum ccode      condition;              /* the condition code, if Jcc/SETcc */
    int             operands;               /* how many operands? 0-3 (more if db et al) */
    int             addr_size;              /* address size */
    operand         oprs[MAX_OPERANDS];     /* the operands, defined as above */
    extop           *eops;                  /* extended operands */
    int             eops_float;             /* true if DD and floating */
    int32_t         times;                  /* repeat count (TIMES prefix) */
    bool            forw_ref;               /* is there a forward reference? */
    bool            rex_done;               /* REX prefix emitted? */
    int             rex;                    /* Special REX Prefix */
    int             vexreg;                 /* Register encoded in VEX prefix */
    int             vex_cm;                 /* Class and M field for VEX prefix */
    int             vex_wlp;                /* W, P and L information for VEX prefix */
    uint8_t         evex_p[3];              /* EVEX.P0: [RXB,R',00,mm], P1: [W,vvvv,1,pp] */
                                            /* EVEX.P2: [z,L'L,b,V',aaa] */
    enum ttypes     evex_tuple;             /* Tuple type for compressed Disp8*N */
    int             evex_rm;                /* static rounding mode for AVX512 (EVEX) */
    int8_t          evex_brerop;            /* BR/ER/SAE operand position */
} insn;
static void assemble_file(const char *fname, StrList **depend_ptr)
{
    char *line;
    insn output_ins;
    int i;
    uint64_t prev_offset_changed;
    int64_t stall_count = 0; /* Make sure we make forward progress... */

    [...]

        while ((line = preproc->getline())) {
            if (++globallineno > nasm_limit[LIMIT_LINES])
                nasm_fatal(0,
                           "overall line count exceeds the maximum %"PRId64"\n",
                           nasm_limit[LIMIT_LINES]);

            /*
             * Here we parse our directives; this is not handled by the
             * main parser.
             */
            if (process_directives(line))
                goto end_of_line; /* Just do final cleanup */

            /* Not a directive, or even something that starts with [ */
            parse_line(pass1, line, &output_ins);   <-- [1]

            if (optimizing > 0) {
                if (forwref != NULL && globallineno == forwref->lineno) {
                    output_ins.forw_ref = true;
    [...]

In assemble_file function, there is an object (output_ins) to a structure (struct insn) which contains the informations regarding the opcode being parsed. Inside the assemble_file, a function parse_line is called in order initialize the object.

insn *parse_line(int pass, char *buffer, insn *result)
{
    bool insn_is_label = false;
    struct eval_hints hints;
    int opnum;
    int critical;
    bool first;
    bool recover;
    int i;

    nasm_static_assert(P_none == 0);

restart_parse:
    first               = true;
    result->forw_ref    = false;

    stdscan_reset();
    stdscan_set(buffer);
    i = stdscan(NULL, &tokval);

    memset(result->prefixes, P_none, sizeof(result->prefixes));
    result->times       = 1;    /* No TIMES either yet */
    result->label       = NULL; /* Assume no label */       <-- [2]
    result->eops        = NULL; /* must do this, whatever happens */
    result->operands    = 0;    /* must initialize this */
    result->evex_rm     = 0;    /* Ensure EVEX rounding mode is reset */
    result->evex_brerop = -1;   /* Reset EVEX broadcasting/ER op position */
Here the structure’s member label which should be containing the definition of the label is clearly being assigned to NULL.

    [...]

    if (i == TOKEN_ID || (insn_is_label && i == TOKEN_INSN)) {    <-- not taken
        /* there's a label here */
        first = false;
        result->label = tokval.t_charptr;
        i = stdscan(NULL, &tokval);
        if (i == ':') {         /* skip over the optional colon */
            i = stdscan(NULL, &tokval);
        } else if (i == 0) {
            nasm_error(ERR_WARNING | ERR_WARN_OL | ERR_PASS1,
                  "label alone on a line without a colon might be in error");
        }
        if (i != TOKEN_INSN || tokval.t_integer != I_EQU) {
            /*
             * FIXME: location.segment could be NO_SEG, in which case
             * it is possible we should be passing 'absolute.segment'. Look into this.
             * Work out whether that is *really* what we should be doing.
             * Generally fix things. I think this is right as it is, but
             * am still not certain.
             */
            define_label(result->label,
                         in_absolute ? absolute.segment : location.segment,
                         location.offset, true);
    [...] 

However, down to a few lines of code, there is a check before the it gets initialized with a valid value which is then skipped because the boolean insn_is_label is always FALSE. Therefore, the result->label remains NULL.

    [...]

            /*  forw_ref */
            if (output_ins.opcode == I_EQU) {
                if (!output_ins.label)
                    nasm_error(ERR_NONFATAL,
                               "EQU not preceded by label");

                if (output_ins.operands == 1 &&
                    (output_ins.oprs[0].type & IMMEDIATE) &&
                    output_ins.oprs[0].wrt == NO_SEG) {
                    define_label(output_ins.label,
                                 output_ins.oprs[0].segment,
                                 output_ins.oprs[0].offset, false);   <-- [3]
    [...]

So back to the assemble_line, there is a check if the opcode is EQU and surprisingly the nasm_error did not handle the error safely (marked as ERR_NONFATAL). The output_ins.label is then being passed into 3 functions before it gets dereferenced (define_label -> find_label -> islocal).

static bool islocal(const char *l)
{
    if (tasm_compatible_mode) {
        if (l[0] == '@' && l[1] == '@')
            return true;
    }
    return (l[0] == '.' && l[1] != '.');  <-- boom (NULL Pointer)
}