5. Fuzzing

Our instrumented sudo now expects AFL-style argv[] input from stdin. That means fuzzing is as simple as:

Bash
afl-fuzz -i in/ -o tmp/ -- $HOME/fuzz/proj/sudo-1.9.5p1/harness/bin/sudo

Here, in/ contains our seed corpus (null-delimited argv files), and out/ is the fuzzer's crash + coverage stash.

5.1. Parallel Fuzzing

One AFL instance = one CPU core. To actually rip through paths, we need parallel fuzzing: multiple fuzzers working in sync, sharing a queue of test cases.

Pro tip: to speed up file I/O and avoid wearing out SSDs, we can place the output directory on a RAM-backed filesystem (tmpfs).

5.1.1. AFL Luancher

I use my own afl_launcher.py to spin up a cluster of AFL++ instances inside Tmux:

fuzz_sudo_1-11
Bash
afl_launcher.py -i in/ -o out -debug -- ./harness/bin/sudo 

If you don't have a custom launcher, it's trivial to roll one (see Gamozolabs' scaling post).

This opens a curses-style master window plus silent slaves, burning all CPU cores like a distributed brute-force engine.

fuzz_sudo_1-12

Other slave fuzzers are recorded by afl-whatsup:

fuzz_sudo_1-13

5.1.2. Manual

AFL++ supports distributed fuzzing via the -M (master) and -S (slave) flags:

  • Master (-M): does deterministic stages + queue pruning.
  • Slaves (-S): skip deterministic stages, focus on raw speed.

Pin instances to cores with either taskset -c or AFL's -b binding option.

Master:

Bash
# Master pinned to core 0 using taskset -c
taskset -c 0 afl-fuzz -i in/ -o out -M m -- harness/bin/sudo

# Or, use AFL++ bind option
afl-fuzz -i in/ -o out -M m -b 0 -- harness/bin/sudo

Slaves:

Bash
afl-fuzz -i in/ -o out -M s1 -b 1 -- harness/bin/sudo
afl-fuzz -i in/ -o out -M s2 -b 2 -- harness/bin/sudo
afl-fuzz -i in/ -o out -M s3 -b 3 -- harness/bin/sudo

This runs 1 master + 3 slaves across cores 0–3.

Automated loop:

Bash
export ncpu=10	# Specify number of CPU we want to allocate

for i in $(seq 0 $ncpu); do
role=$([ $i -eq 0 ] && echo "-M m" || echo "-S s$i")
taskset -c $i afl-fuzz -i in/ -o out $role -- harness/bin/sudo > out/log_$i.txt 2>&1 &
done

Verify with:

Bash
ps -o pid,psr,comm -C afl-fuzz

This shows which core each fuzzer is pinned to—no freeloaders.

5.2. Result

Total run time will be calculated accumulatively by master and all slave fuzzers:

fuzz_sudo_1-14

5.2.1. Crashes

After hours of AFL++ hammering both sudo and sudoedit, the crash harvest came in. Unsurprisingly, sudoedit yielded far more interesting results — its argument parsing is fragile, and AFL loved poking it.

$ tree out

out
├── log_master_0.err
├── log_slave_1.err
├── log_slave_2.err
├── log_slave_3.err
├── log_slave_4.err
├── log_slave_5.err
├── log_slave_6.err
├── log_slave_7.err
├── master_0
│   ├── cmdline
│   ├── crashes
│   │   ├── id:000000,sig:06,src:000153,time:118435,execs:48008,op:havoc,rep:5
│   │   ├── id:000001,sig:06,src:000153,time:159190,execs:50795,op:havoc,rep:5
│   │   ├── id:000002,sig:06,src:000153,time:206612,execs:55008,op:havoc,rep:3
│   │   ├── id:000003,sig:06,src:000598,time:5496912,execs:115120,op:havoc,rep:1
│   │   ├── id:000004,sig:06,src:000598,time:5497094,execs:115277,op:havoc,rep:4
│   │   ├── id:000005,sig:06,src:000283,time:8289828,execs:175023,op:havoc,rep:9
│   │   └── README.txt
│   ├── fastresume.bin
│   ├── fuzz_bitmap
│   ├── fuzzer_setup
│   ├── fuzzer_stats
│   ├── hangs
│   │   ├── id:000000,src:000153,time:108895,execs:47304,op:havoc,rep:5
│   │   ├── id:000001,src:000153,time:117878,execs:47682,op:havoc,rep:8
│   │   ├── id:000002,src:000153,time:126817,execs:48181,op:havoc,rep:5
│   │   ...
│   ...
├── slave_1
│   ├── cmdline
│   ├── crashes
│   │   ├── id:000000,sig:06,src:000259,time:136526,execs:139505,op:havoc,rep:3
│   │   ├── id:000001,sig:06,src:000283,time:161925,execs:160135,op:havoc,rep:3
│   │   ├── id:000002,sig:06,src:000304,time:172998,execs:169905,op:havoc,rep:10
│   │   ├── id:000003,sig:06,src:000289,time:246233,execs:236175,op:havoc,rep:8
│   │   └── README.txt
│   ├── fastresume.bin
│   ├── fuzz_bitmap
│   ├── fuzzer_setup
│   ├── fuzzer_stats
│   ├── hangs
│   │   ├── id:000000,src:000395,time:273506,execs:258140,op:havoc,rep:2
│   │   ├── id:000001,src:000395,time:277634,execs:258147,op:havoc,rep:2
│   │   ├── id:000002,src:000395,time:285204,execs:259220,op:havoc,rep:1
│   │   ...
...

32 directories, 5794 files

ASan confirmed it: classic heap-buffer-overflow triggered inside sudoedit:

fuzz_sudo_1-15

5.2.2. Report Analysis

The crash trace points to set_cnmd:

==56190==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x603000000e10
WRITE of size 1 at 0x603000000e10 thread T0
#0 0x555555834c7b in set_cmnd ...sudoers.c:976:13
#1 ...

The AddressSanitizer (ASan) report a classic heap-buffer-overflow.

The call stack clearly shows where program started → where it crashed:

#0 0x555555834c7b in set_cmnd ...src/plugins/sudoers/./sudoers.c:976:13
#1 0x555555834c7b in sudoers_policy_main ...src/plugins/sudoers/./sudoers.c:401:19
#2 0x555555803d25 in sudoers_policy_check ...src/plugins/sudoers/./policy.c:1028:11
#3 0x555555760787 in policy_check .../src/./sudo.c:1179:10
#4 0x555555759f29 in main .../src/./sudo.c:277:9
#5 0x7ffff65b0c86 in __libc_start_main /build/glibc-CVJwZb/glibc-2.27/csu/../csu/libc-start.c:310
#6 0x555555644bf9 in _start (.../harness/bin/sudo+0xf0bf9) 

The bad pointer came from a malloc call:

0x603000000e10 is located 0 bytes to the right of 32-byte region [0x603000000df0,0x603000000e10)
allocated by thread T0 here:
    #0 0x5555556c97de in malloc (.../harness/bin/sudo+0x1757de) 
    #1 0x55555582f634 in set_cmnd .../plugins/sudoers/./sudoers.c:960:36
  • The binary allocated 32 bytes at 0x603000000df0
  • But then wrote to 0x603000000e10 → 1 byte past the end
  • The malloc happened 16 lines before the crash, at line 960

ASAN - SHADOW MEMORY

ASan maps each 8 bytes of our application's memory to 1 byte in shadow memory. That 1 byte indicates whether the corresponding memory is:

  • Fully addressable (00)
  • Partially addressable (01 to 07)
  • Unaddressable / poisoned (fa, fd, etc.)

This mapping lets ASan detect reads/writes to invalid regions like redzones, freed chunks, etc. In our sample output:

=>0x0c067fff81c0: 00 00[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa

This line says:

  • The application memory at 0x603000000e10 maps to shadow byte fa at 0x0c067fff81c2.
  • [fa] means the first byte of unaddressable (poisoned) memory.
  • Our overflow write hit this poisoned redzone → ASan traps it.

5.2.3. Payload Distillation

From the crash corpus:

$ cat out/master_0/crashes/id:000006,sig:06,src:000250,time:225178,execs:57059,op:havoc,rep:3
sduQagUtsufo-nki-o\doo"""do%                                    

$ xxd -g1 out/master_0/crashes/id:000006,sig:06,src:000250,time:225178,execs:57059,op:havoc,rep:3 
00000000: 73 7f 64 75 51 61 67 55 74 73 75 66 6f 00 2d 6e  s.duQagUtsufo.-n
00000010: 6b 69 00 01 00 2d 00 02 00 6f 00 02 00 5c 00 02  ki...-...o...\..
00000020: 00 02 64 6f 18 02 00 02 00 02 00 6f 00 02 00 22  ..do.......o..."
00000030: 22 22 02 02 64 6f 00 02 00 02 00                 ""..do.....

Translation:

<argv[0]> -nki - o '\' junk_string \"\"\" junk_string 

The first fuzzed argv[0] does not matter in our test—we stemmed it as "sudoedit" by the AFL_INIT_SET0("sudoedit") macro when collecting this bug sample. Pay attention to some special chars like \ or ", which might be the cause triggering unexpected errors.

Test to find out the collision command:

Bash
./install/bin/sudoedit -nki - o '\' somestringaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

We can run it with the original non-modified sudoedit command to verify this crash without passing a correct password:

fuzz_sudo_1-16

Same error achieved. Narrow down the payload scope, we reach a minimal affected version:

$ ./install/bin/sudoedit -i '\' somestringaaaaaaaaaaaaaaaaaaaaa
malloc(): memory corruption
[1]    63258 abort      ./install/bin/sudoedit -i '\' somestringaaaaaaaaaaaaaaaaaaaaa

$ ./install/bin/sudoedit -s '\' somestringaaaaaaaaaaaaaaaaaaaaa
malloc(): memory corruption
[1]    72593 abort      ./install/bin/sudoedit -s '\' somestringaaaaaaaaaaaaaaaaaaaaa

The heap corruption appears when sudoedit is invoked with -i or -s plus two extra args:

  • The first being a literal backslash (\).
  • The second being a sufficiently long string (≥10 bytes).

Minimal reproducer (for this stage):

Bash
sudoedit -i '\' aaaaaaaaaaa
sudoedit -s '\' aaaaaaaaaaa

At that point, set_cmnd() miscalculates buffer space and overruns malloc'd memory.