Special thanks to Fakhri (@d0lph1n98) for helping out with the analysis :)
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.
echo "equ push rax" > poc
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)
}