[EN] A-Z: A is for Alpine

A-Z project

Some time ago we got the idea to select projects alphabetically, pick a project for each letter from a to z and find at least one bug in it. We don’t have a strict plan, rules or deadlines. We treat it is a freestyle battle and we just test whatever we want to. Our goal is to try different approaches including many flavours of fuzzing, code review, static analysis, dynamic analysis, divination etc. We’re going to write not only about what worked for us, but also about our failures. We plan to issue posts on each letter randomly, so here’s the first post for the letter A!

A is for Alpine.

Alpine is a terminal e-mail client which was created at the University of Washington as a Pine rewrite. The truth behind this choice is that our friend is a hard-core alpine user, while some of us are in the mutt camp. So we thought that it would be fun to crash his favourite utility evil_laugh.wav.

wikipedia

image source

Approach

Command line fuzzing

We tried to take a simpliest possible approach to cause crashes and detect them.

To generate test cases we decided to use the greatest mutator available - Radamsa. It is just a binary, so we created a simple python wrapper:

class Radamsa():

    (...)

    def radamsa(self, val):
        seed = 0
        with self.threadLock:
            seed = self.seed
            self.seed += 1
        o = subprocess.check_output("echo '" + val + "' | /root/fuzz/radamsa/bin/radamsa -n 1 --seed " + str(seed), shell=True)
        return (o, seed)

As Alpine has many options, we chose a few ones which seemed to have the greatest potential:

       -I keystrokes       Initial (comma separated list of) keystrokes which Alpine should execute on startup.
       -url url            Open the given url.  Cannot be used with -f or -F options.
       -option=value       Assign value to the config option option e.g. -signature-file=sig1 or -feature-list=signature-at-bottom (Note: feature-list values are additive)

Detection was based on AddressSanitizer (ASan) - memory error detector, which have to enabled during compilation:

CFLAGS="-fsanitize=address -O0 -ggdb" C_CLIENT_CFLAGS="-fsanitize=address -O0 -ggdb" LDFLAGS="-lasan" C_CLIENT_LDFLAGS="-lasan" ./configure
export ASAN_OPTIONS=detect_leaks=0
make

ASan also required runtime configuration, to save crashes with names allowing to track corresponding test case and its payload to repeat a crash.

name = "confalp{}s{}r{}".format(self.pattern_id, seed, r)
"ASAN_OPTIONS=log_path=/root/fuzz/asan_logs/{}".format(name)

The last step was to run Alpine from the script. It wasn’t so easy as just executing a binary with generated options, because Alpine runs in an interactive mode and some options may cause a problem after a while. Passing the process to the background (& at the end of a command) wasn’t an options because the binary paused execution. So we utilized GNU Screen. We would start Alpine inside a screen, give one second to crash and then kill a running screen.

path = "/root/fuzz/alpine-2.21/alpine/alpine"
(...)
payload = self.radamsa(self.pattern)
(...)
os.system("screen -S {} -dm {} -'{}'".format(name, path, payload))
time.sleep(1)
os.system("screen -X -S {} quit".format(name))

Final scripts:

Mailbox fuzzing

Our another take was to try crash alpine via mailbox, so we crafted a super simple script with a hope that we’ll find something interesting in the parsing. So we downloaded tons of spam and tried to mutate it with radamsa.

#!/bin/bash

~/src/radamsa/bin/radamsa spam > x 
~/src/alpine/alpine/alpine -i -f x < `tty` & 
sleep 1 
killall -9 alpine
reset

We didn’t found anything, BUT we found a bug with a non-tty file passed a stdin which is described below. :)

Found bugs

After some time of executing the scripts, crashes became to appear. From many repetitive crashes, we extracted a few unique ones.

  • empty url fragment:
alpine -url #
  • invalid format of last-time-prune-questioned option:
alpine -last-time-prune-questioned=116,3
  • printf formatting characters in option name:
alpine '-per\x!s\x0c0$$a%!"cln\x00!xcalc+inf+inf%s%s!xcalc\x0d;xcalc!xcalc$&\r!!sonal-name=YOUR NAME'
  • extremely long option name:
alpine '-postponed-fold/Mails/Fastmail/INBOX.Drastmails/Fastmails/Fastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOrastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drastmail/INBOX.Drail/INBOX.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Dra.Drafts'
  • crash on file that is not a tty:
alpine -i -f ABCD < /dev/null

Quick analysis of the last bug

(gdb) bt
#0  strlen () at ../sysdeps/x86_64/strlen.S:106
#1  0x00007ffff6eda1a6 in strlen () from /usr/lib/x86_64-linux-gnu/libasan.so.2
#2  0x000000000045bb23 in main (argc=4, argv=0x7fffffffe608) at alpine.c:946

the problem is in alpine.c:

925         else if(args.action == aaMail || (stdin_getc && (args.action != aaURL))){
926             /*======= address on command line/send one message mode ============*/
927             char       *to = NULL, *error = NULL, *addr = NULL;
928             int         len, good_addr = 1;
929             int         exit_val = 0;
930             BUILDER_ARG fcc;
[...]
941             /*----- Format the To: line with commas for the composer ---*/
942             if(args.data.mail.addrlist){
943                 STRLIST_S *p;
944
945                 for(p = args.data.mail.addrlist, len = 0; p; p = p->next)
946                   len += strlen(p->name) + 2;

if condition is passed by stdin_getc != NULL and args.action = aaFolder:

(gdb) print args.action
$12 = aaFolder
(gdb) print stdin_getc
$13 = (gf_io_t) 0x45ed30 <read_stdin_char>

stdin_getc is set when stdin is not a tty, args.action is set because of -f switch which copies data to args.data:

(gdb) ptype args.data
type = union {
    char *folder;
    char *file;
    struct {   
        STRLIST_S *addrlist;
        PATMT *attachlist;
    } mail;
    struct {
        char *local;
        char *remote;
    } copy;
}
(gdb)

the problem is that it’s an union and code internally uses args.data.mail.addrlist which points to the string:

(gdb) print &args.data.folder
$14 = (char **) 0x7fffffffb778
(gdb) print &args.data.mail.addrlist
$15 = (STRLIST_S **) 0x7fffffffb778

(gdb) x/s args.data.mail.addrlist
0x60200000ee10: "ABCDEFGH"

(gdb) print *args.data.mail.addrlist
$17 = {
  name = 0x4847464544434241 <error: Cannot access memory at address 0x4847464544434241>,
+next = 0x0}

So the bug seems to be caused by the confusion of args.data usage. Exploitability we left as an exercise for you, dear reader!

Summary

For the four bugs we created patches. These patches and detailed description of the last crash were sent to the developer responsible for Alpine project. He quickly reacted reviewing and accepting our patches and fixing the fifth crash within a few days! Much more can be done with Alpine, as we didn’t found a way to crash it from the remote.

https://repo.or.cz/alpine.git/commit/3443fe5fcfcb33d3a2510111855e619632de57df

Stay tuned for the B issue!

Written on August 27, 2019 by Mateusz Kocielski, Michał Dardas