Case Study: Security Analysis of Halibut


Over the past year I've been studying memory corruption vulnerabilities in Linux C/C++ programs, culminating in the open sourcing of a framework called ARCUS to find and explain them automatically using a combination of dynamic tracing and symbolic analysis. My work has led to two academic conference publications, one that appeared in this year's USENIX Security Symposium and another that will appear next month at ACM CCS.

Since then, I've been going through the Debian Popularity Contest and analyzing packages, leading to the discovery of vulnerabilities like CVE-2021-42006. In this post, I'd like to share 3 new vulnerabilities I've discovered in a program called Halibut, which is a document preparation system that currently sits at Rank 54,752 (by number of installs) out of 182,832 packages in the popularity contest.

Environment

I'm using the latest official release of Halibut, version 1.2, which is available on Debian Bullseye and Ubuntu Hirsute, to name a few Linux distributions. The target architecture is x86-64.

Double Free in cleanup_index() in index.c

Steps to Reproduce:

  1. Download the PoC.
  2. Run: halibut --winhelp poc-halibut-winhelp-df

Stack Trace:

#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007ffff7e1c537 in __GI_abort () at abort.c:79
#2  0x00007ffff7e75768 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7ffff7f83e2d "%s\n") at ../sysdeps/posix/libc_fatal.c:155
#3  0x00007ffff7e7ca5a in malloc_printerr (str=str@entry=0x7ffff7f861c8 "double free or corruption (fasttop)") at malloc.c:5347
#4  0x00007ffff7e7dd55 in _int_free (av=0x7ffff7fb5b80 <main_arena>, p=0x5555556a5500, have_lock=0) at malloc.c:4266
#5  0x00005555555784c5 in sfree (p=0x5555556a5510) at ../malloc.c:63
#6  0x000055555557867d in free_word_list (w=0x5555556ab7b0) at ../malloc.c:130
#7  0x0000555555589fd6 in cleanup_index (i=0x5555556a2390) at ../index.c:203
#8  0x0000555555578154 in main (argc=0, argv=0x7fffffffe468) at ../main.c:404

Use-After-Free in cleanup_index() in index.c

Steps to Reproduce:

  1. Download the PoC.
  2. Run: halibut --text poc-halibut-text-uaf

Stack Trace:

#0  __GI___libc_free (mem=0x100000001) at malloc.c:3102
#1  0x00005555555784c5 in sfree (p=0x100000001) at ../malloc.c:63
#2  0x000055555557867d in free_word_list (w=0x7000700070007) at ../malloc.c:130
#3  0x000055555557869a in free_word_list (w=0x5555556f1900) at ../malloc.c:132
#4  0x0000555555589fd6 in cleanup_index (i=0x5555556a2390) at ../index.c:203
#5  0x0000555555578154 in main (argc=0, argv=0x7fffffffe478) at ../main.c:404

Note: This is not the same vulnerability as the one above. Notice the recursion inside free_word_list and how the PoC triggers a segmentation fault rather than a double free abort.

Use-After-Free in info_width_internal() in bk_info.c

Steps to Reproduce:

  1. Download the PoC.
  2. Run: halibut --info poc-halibut-info-uaf

Stack Trace:

#0  info_width_internal (words=0x5555556e92a0, xrefs=1, cfg=0x7fffffffdf80) at ../bk_info.c:974
#1  0x000055555559c419 in info_width_internal_list (words=0x5555556e92a0, xrefs=1, cfg=0x7fffffffdf80) at ../bk_info.c:953
#2  0x000055555559c669 in info_width_internal (words=0x5555556ed5b0, xrefs=1, cfg=0x7fffffffdf80) at ../bk_info.c:1009
#3  0x000055555559c776 in info_width_xrefs (ctx=0x7fffffffdf80, words=0x5555556ed5b0) at ../bk_info.c:1041
#4  0x000055555557b0cb in wrap_para (text=0x5555556ed5b0, width=66, subsequentwidth=66, widthfn=0x55555559c751 <info_width_xrefs>, ctx=0x7fffffffdf80, natural_space=0) at ../misc.c:328
#5  0x000055555559cab8 in info_para (text=0x5555556e8440, prefix=0x5555556a2d30, prefixextra=0x5555555c859c L".", input=0x5555556bcf80, keywords=0x5555556e1cd0, indent=1, extraindent=3, width=66, 
    cfg=0x7fffffffdf80) at ../bk_info.c:1120
#6  0x000055555559b38a in info_backend (sourceform=0x5555556a7dd0, keywords=0x5555556e1cd0, idx=0x5555556a2390, unused=0x0) at ../bk_info.c:579
#7  0x0000555555578119 in main (argc=0, argv=0x7fffffffe478) at ../main.c:398

Root Cause

According to ARCUS, the root cause appears to be frees that occur within get_token in input.c. Specifically, it labels Line 416:

       if (rsc.text) {
            in->pushback_chars = dupstr(rsc.text + prevpos);
            sfree(rsc.text);
        }

And Line 469:

    } else if (c == '{') {             /* tok_lbrace */
        ret.type = tok_lbrace;
        sfree(rsc.text);
        return ret;

There are several other lines in get_token that also call sfree and might be problematic.