CVE-2021-3156 sudo堆溢出 漏洞分析

漏洞简介

sudo 是 linux 系统管理指令,是允许系统管理员让普通用户执行一些或者全部的 root 命令的一个工具,它允许授权用户以 root 权限执行命令或者程序。

sudo 的 sudoer 插件里面存在一个堆溢出漏洞,攻击者可以利用该漏洞进行提权

影响范围

all legacy versions from 1.8.2 to 1.8.31p2
all stable versions from 1.9.0 to 1.9.5p1

漏洞分析

该漏洞位于 sudoers.c: set_cmnd 函数

static int
set_cmnd(void)
{
struct sudo_nss *nss;
char *path = user_path;
int ret = FOUND;


    /* 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(-1);
    }
    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++) { // NewArgv是一个二维数组,下面拷贝这个NewArgv到user_args
        while (*from) { //判断是否到字符串结尾
        if (from[0] == '\\' && !isspace((unsigned char)from[1])) //[1]
            from++;//[2]
        *to++ = *from++;//[3]
        }
        *to++ = ' ';
    }
    *--to = '\0';

debug_return_int(ret);

}
当 21-28 行代码处理以\ 为结尾或者作为一个单独的参数时

from[0] 指向\ ,而 from[1] 指向\x00 ,\x00被拷贝到 user_args 中

然后 from++ 指向 user_args 后面的内存,再一次进入到 while 循环中,越界拷贝后面的的数据到 user_args 中。

举个例子

启动命令如下

sudo -s ‘’ ‘aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa’

图片
当from[0]指向0x7ffcd8c9415d时,from[1] 会指向\x00 然后把 \x00拷贝到 user_args 中

然后 from++ 指向第二个参数的 data,再次进入到 while 循环中,把第二个参数的 data 拷贝到 user_args 中

执行完第一次 for 循环,av++ 这时的 from 指向第二个参数,进行第二个参数的拷贝

在拷贝第二个参数时,user_args 的分配的内容就已经用完,这时就溢出修改 user_args 后面的数据

NewArgv 后面接着是一些环境变量,可以利用环境变量来控制溢出的数据,(比如想要写\x00 可以在环境变量里面加很多\ )

利用

从漏洞的类型,user_args 的数据类型和 sudo 本身进行思考

这个漏洞是堆溢出
这个漏洞无法泄露数据
从这两点可以看出,这个漏洞的利用很大的可能就是覆盖 user_args 后面的结构体或者函数地址(一般函数地址在 heap 上的,估摸就是结构体内部的成员)

要实现溢出修改目标结构体,需要控制堆上的内存布局。Qualys团队提到了一种方法,控制传入 sudo 的LC环境变量

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

setlocale函数里面调用了 malloc 和 free,在 malloc 和 free 的过程中,产生了许多 hole,可以通过这些 hole 来控制内存布局,使得想要溢出控制的堆块落在 user_args 后面

Qualys团队提出了三种利用方法

本次分析 Qualys团队 里面介绍的第二种和第三种利用方法

1、struct service_user overwrite

这个利用方法是覆盖 service_user 结构体,控制 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,//[1]
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,//[2]
                      "libnss_"),
                ni->name),
          ".so"),
    __nss_shlib_revision);

  ni->library->lib_handle = __libc_dlopen (shlib_name);//[3]
  if (ni->library->lib_handle == NULL)
{
  /* Failed to load the library.  */
  ni->library->lib_handle = (void *) -1l;
  __set_errno (saved_errno);
}

//*************************

return 0;
}
service_user 结构

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;
利用思路

首先覆盖 library == NULL,然后就进入到 [1] ,这样就能保证 ni->library->lib_handle == NULL

static service_library *
nss_new_service (name_database *database, const char *name)
{
service_library **currentp = &database->library;

while (*currentp != NULL)
{
if (strcmp ((*currentp)->name, name) == 0)
return *currentp;
currentp = &(*currentp)->next;
}

/* We have to add the new service. */
*currentp = (service_library *) malloc (sizeof (service_library));
if (*currentp == NULL)
return NULL;

(*currentp)->name = name;
(currentp)->lib_handle = NULL; //[]
(*currentp)->next = NULL;

return *currentp;
}
在 [2] 组装 __libc_dlopen 的传入参数 shlib_name,因为 ni->name 是可控的,所以 shlib_name 也是可控的

在 [3] 加载 so 文件,因为 shlib_name 可控,所以可以任意加载 so 文件。在编写 so 文件的代码时,需要在执行的函数前加上 _attribute((constructor)) 这个属性

这是 gcc 的一个属性,加这个属性的函数,会在 __libc_dlopen 返回之前执行,就有点类似于 c++ 的构造函数

Initialization and finalization functions
Shared objects may export functions using the attribute((constructor)) and attribute((destructor)) function attributes.Constructor functions are executed before dlopen() returns, and destructor functions are executed before dlclose() returns. A shared object may export multiple constructors and destructors, and priorities can be associated with each function to determine the order in which they are executed.See the gcc info pages (under “Function attributes”) for fur‐ther information.
调试

首先需要测试出,执行完 set_cmnd 后第一个 dl_open 的目标

在 nss_load_library 下断点,然后执行完 set_cmnd 就断在 nss_load_library ,测试哪一个 service_user 结构体会在执行完 set_cmnd 之后,在 nss_load_library 中调用 dl_open 加载 so 文件

图片
经调试发现,是 systemd,最后加载的是 libnss_systemd.so.2,所以需要控制这个 ni->name 为 systemd 的结构体,需要使用到前面所说在LC环境变量来控制堆上的 bin 的分布

这个 ni->name 为 systemd 的结构体需要控制在 fastbin,largebin ,tcache 的 heap 的附近,这样在分配 user_args 时,就能分配到这个结构体前面

2、def_timestampdir overwrite

def_timestampdir

#define def_timestampdir (sudo_defs_table[I_TIMESTAMPDIR].sd_un.str)
这个函数里面引用到了 def_timestampdir

int
check_user(int validated, int mode)
{
struct getpass_closure closure = { TS_ERROR };
int ret = -1;
bool exempt = false;
debug_decl(check_user, SUDOERS_DEBUG_AUTH)

/*
 * Init authentication system regardless of whether we need a password.
 * Required for proper PAM session support.
 */
if ((closure.auth_pw = get_authpw(mode)) == NULL)
goto done;
if (sudo_auth_init(closure.auth_pw) == -1)
goto done;

/*
 * Don't prompt for the root passwd or if the user is exempt.
 * If the user is not changing uid/gid, no need for a password.
 */
if (!def_authenticate || user_is_exempt()) {
sudo_debug_printf(SUDO_DEBUG_INFO, "%s: %s", __func__,
    !def_authenticate ? "authentication disabled" :
    "user exempt from authentication");
exempt = true;
ret = true;
goto done;
}
if (user_uid == 0 || (user_uid == runas_pw->pw_uid &&
(!runas_gr || (sudo_user.pw, runas_gr->gr_name)))) {

#ifdef HAVE_SELINUX
if (user_role == NULL && user_type == NULL)
#endif
#ifdef HAVE_PRIV_SET
if (runas_privs == NULL && runas_limitprivs == NULL)
#endif
{
sudo_debug_printf(SUDO_DEBUG_INFO,
“%s: user running command as self”, func);
ret = true;
goto done;
}
}

ret = check_user_interactive(validated, mode, &closure);

done:
if (ret == true) {
/* The approval function may disallow a user post-authentication. */
ret = sudo_auth_approval(closure.auth_pw, validated, exempt);

/*
 * Only update time stamp if user validated and was approved.
 * Failure to update the time stamp is not a fatal error.
 */
if (ret == true && closure.tstat != TS_ERROR) {
    if (ISSET(validated, VALIDATE_SUCCESS))
    (void)timestamp_update(closure.cookie, closure.auth_pw);
}
}
timestamp_close(closure.cookie);
sudo_auth_cleanup(closure.auth_pw);
if (closure.auth_pw != NULL)
sudo_pw_delref(closure.auth_pw);

debug_return_int(ret);

}
要进入到这个函数要在启动命令多加一个 -A,然后需要设置SUDO_ASKPASS环境变量

SUDO_ASKPASS=/bin/false
这里用gdb调试

chmod u+s /usr/bin/gdb
利用思路

那就从 check_user 函数开始

check_user 里面会有个验证 uid,这个 uid 是存储在 sudo 里面的一个全局的结构体 sudo_user 里面

if (user_uid == 0 || (user_uid == runas_pw->pw_uid && //[1]
(!runas_gr || (sudo_user.pw, runas_gr->gr_name)))) {

#ifdef HAVE_SELINUX
if (user_role == NULL && user_type == NULL)
#endif
#ifdef HAVE_PRIV_SET
if (runas_privs == NULL && runas_limitprivs == NULL)
#endif
{
sudo_debug_printf(SUDO_DEBUG_INFO,
“%s: user running command as self”, func);
ret = true;
goto done;
}
}

ret = check_user_interactive(validated, mode, &closure);//[2]

因为 [1] 处 user_uid=1000,然后后面的判断也不成立,就进入了 [2]

static int
check_user_interactive(int validated, int mode, struct getpass_closure *closure)
{
struct sudo_conv_callback cb, *callback = NULL;
int ret = -1;
char *prompt;
bool lectured;
debug_decl(check_user_interactive, SUDOERS_DEBUG_AUTH)

/* Open, lock and read time stamp file if we are using it. */
if (!ISSET(mode, MODE_IGNORE_TICKET)) {
/* Open time stamp file and check its status. */
closure->cookie = timestamp_open(user_name, user_sid); //[1]
if (timestamp_lock(closure->cookie, closure->auth_pw))//[2]
    closure->tstat = timestamp_status(closure->cookie, closure->auth_pw);

/* Construct callback for getpass function. */
memset(&cb, 0, sizeof(cb));
cb.version = SUDO_CONV_CALLBACK_VERSION;
cb.closure = closure;
cb.on_suspend = getpass_suspend;
cb.on_resume = getpass_resume;
callback = &cb;
}

switch (closure->tstat) {
case TS_FATAL:
/* Fatal error (usually setuid failure), unsafe to proceed. */
goto done;

case TS_CURRENT:
/* Time stamp file is valid and current. */
if (!ISSET(validated, FLAG_CHECK_USER)) {
    ret = true;
    break;
}
sudo_debug_printf(SUDO_DEBUG_INFO,
    "%s: check user flag overrides time stamp", __func__);
/* FALLTHROUGH */

default:
/* Bail out if we are non-interactive and a password is required */
if (ISSET(mode, MODE_NONINTERACTIVE)) {
    validated |= FLAG_NON_INTERACTIVE;
    log_auth_failure(validated, 0);
    goto done;
}

/* XXX - should not lecture if askpass helper is being used. */
lectured = display_lecture(closure->tstat);

/* Expand any escapes in the prompt. */
prompt = expand_prompt(user_prompt ? user_prompt : def_passprompt,
    closure->auth_pw->pw_name);
if (prompt == NULL)
    goto done;

ret = verify_user(closure->auth_pw, prompt, validated, callback);
if (ret == true && lectured)
    (void)set_lectured();   /* lecture error not fatal */
free(prompt);
break;
}

done:
debug_return_int(ret);
}
重点在 [1] [2]

timestamp_open(const char *user, pid_t sid)
{
struct ts_cookie *cookie;
char *fname = NULL;
int tries, fd = -1;
debug_decl(timestamp_open, SUDOERS_DEBUG_AUTH)

/* Zero timeout means don't use the time stamp file. */
if (!sudo_timespecisset(&def_timestamp_timeout)) {
errno = ENOENT;
goto bad;
}

/* Sanity check timestamp dir and create if missing. */
if (!ts_secure_dir(def_timestampdir, true, false))//[1]
goto bad;

/* Open time stamp file. */
if (asprintf(&fname, "%s/%s", def_timestampdir, user) == -1) { //[2]
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
goto bad;
}
for (tries = 1; ; tries++) {
struct stat sb;
// 上面能看到 (asprintf(&fname, "%s/%s", def_timestampdir, user)  这个是配置 stampfile的文件名字的
fd = ts_open(fname, O_RDWR|O_CREAT);
switch (fd) {
case TIMESTAMP_OPEN_ERROR:
    log_warning(SLOG_SEND_MAIL, N_("unable to open %s"), fname);
    goto bad;
case TIMESTAMP_PERM_ERROR:
    /* Already logged set_perms/restore_perms error. */
    goto bad;
}

/* Remove time stamp file if its mtime predates boot time. */
if (tries == 1 && fstat(fd, &sb) == 0) {
    struct timespec boottime, mtime, now;

    if (sudo_gettime_real(&now) == 0 && get_boottime(&boottime)) {
    /* Ignore a boot time that is in the future. */
    if (sudo_timespeccmp(&now, &boottime, <)) {
        sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
        "ignoring boot time that is in the future");
    } else {
        mtim_get(&sb, mtime);
        if (sudo_timespeccmp(&mtime, &boottime, <)) {
        /* Time stamp file too old, remove it. */
        sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
            "removing time stamp file that predates boot time");
        close(fd);
        unlink(fname);
        continue;
        }
    }
    }
}
break;
}

/* Allocate and fill in cookie to store state. */
cookie = malloc(sizeof(*cookie));
if (cookie == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
goto bad;
}
cookie->fd = fd;
cookie->fname = fname;
cookie->sid = sid;
cookie->pos = -1;

debug_return_ptr(cookie);

bad:
if (fd != -1)
close(fd);
free(fname);
debug_return_ptr(NULL);
}
[1] 是对路径的一个检查,检查路径是否存在,如果存在,权限是否正确

[2] 这里会组装路径,user 就是你的用户的名字,如果发生溢出,然后溢出修改 def_timestampdir,那么 def_timestampdir 就受我们控制

那么现在先说 怎么绕过 ts_secure_dir 的检测

static bool
ts_secure_dir(char *path, bool make_it, bool quiet)
{
struct stat sb;
bool ret = false;
debug_decl(ts_secure_dir, SUDOERS_DEBUG_AUTH)

sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "checking %s", path);
switch (sudo_secure_dir(path, timestamp_uid, -1, &sb)) {//[1]
case SUDO_PATH_SECURE:
ret = true;
break;
case SUDO_PATH_MISSING:
if (make_it && ts_mkdirs(path, timestamp_uid, timestamp_gid, S_IRWXU,//[2]
    S_IRWXU|S_IXGRP|S_IXOTH, quiet)) {
    ret = true;
    break;
}
errno = ENOENT;
break;
case SUDO_PATH_BAD_TYPE:
errno = ENOTDIR;
if (!quiet)
    sudo_warn("%s", path);
break;
case SUDO_PATH_WRONG_OWNER:
if (!quiet) {
    sudo_warnx(U_("%s is owned by uid %u, should be %u"),
    path, (unsigned int) sb.st_uid,
    (unsigned int) timestamp_uid);
}
errno = EACCES;
break;
case SUDO_PATH_GROUP_WRITABLE:
if (!quiet)
    sudo_warnx(U_("%s is group writable"), path);
errno = EACCES;
break;
}
debug_return_bool(ret);

}

static bool
ts_mkdirs(char *path, uid_t owner, gid_t group, mode_t mode,
mode_t parent_mode, bool quiet)
{
bool ret;
mode_t omask;
debug_decl(ts_mkdirs, SUDOERS_DEBUG_AUTH)

/* umask must not be more restrictive than the file modes. */
omask = umask(ACCESSPERMS & ~(mode|parent_mode));
ret = sudo_mkdir_parents(path, owner, group, parent_mode, quiet);
if (ret) {
/* Create final path component. */
sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
    "mkdir %s, mode 0%o, uid %d, gid %d", path, (unsigned int)mode,
    (int)owner, (int)group);
if (mkdir(path, mode) != 0 && errno != EEXIST) { //[3]
    if (!quiet)
    sudo_warn(U_("unable to mkdir %s"), path);
    ret = false;
} else {
    if (chown(path, owner, group) != 0) { //[4]
    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
        "%s: unable to chown %d:%d %s", __func__,
        (int)owner, (int)group, path);
    }
}
}
umask(omask);
debug_return_bool(ret);

}
这里有四步蛮重要的

第一步检测路径和权限,这时候让它不存在,进入 [2]

然后在 [3] 里面会创建 文件夹,这时候需要竞争

首先在 mkdir 之前创建一个文件夹,执行到 mkdir 就报错(但不影响),因为这个 mkdir 创建的文件是 root 权限,如果它创建成功,那后面的符号链接就创建失败

创建的时间需要在 [1] 到[3] 这个时间段 ,这个竞争的成功率蛮高的,

在[4]处会有一个设置文件所有者和文件关联组的操作,这个操作是把目录加入到 root 组,所以不能让它执行成功,这时候需要把目录删除掉,chown 就报错(这个也不影响)

然后这里就绕过了 这个 ts_secure_dir 成功走到 timestamp_open 的下面

/* Open time stamp file. */
if (asprintf(&fname, "%s/%s", def_timestampdir, user) == -1) { //[1]
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
goto bad;
}
for (tries = 1; ; tries++) {
struct stat sb;
// 上面能看到 (asprintf(&fname, "%s/%s", def_timestampdir, user)  这个是配置 stampfile的文件名字的
fd = ts_open(fname, O_RDWR|O_CREAT); //[2]
switch (fd) {
case TIMESTAMP_OPEN_ERROR:
    log_warning(SLOG_SEND_MAIL, N_("unable to open %s"), fname);
    goto bad;
case TIMESTAMP_PERM_ERROR:
    /* Already logged set_perms/restore_perms error. */
    goto bad;
}

/* Remove time stamp file if its mtime predates boot time. */
if (tries == 1 && fstat(fd, &sb) == 0) { //[3]
    struct timespec boottime, mtime, now;

    if (sudo_gettime_real(&now) == 0 && get_boottime(&boottime)) {
    /* Ignore a boot time that is in the future. */
    if (sudo_timespeccmp(&now, &boottime, <)) {
        sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
        "ignoring boot time that is in the future");
    } else {
        mtim_get(&sb, mtime);
        if (sudo_timespeccmp(&mtime, &boottime, <)) {
        /* Time stamp file too old, remove it. */
        sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
            "removing time stamp file that predates boot time");
        close(fd);
        unlink(fname); //[4]
        continue;
        }
    }
    }
}
break;
}

/* Allocate and fill in cookie to store state. */
cookie = malloc(sizeof(*cookie));
if (cookie == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
goto bad;
}
cookie->fd = fd;
cookie->fname = fname;
cookie->sid = sid;
cookie->pos = -1;

debug_return_ptr(cookie);

bad:
if (fd != -1)
close(fd);
free(fname);
debug_return_ptr(NULL);
}
[1] 把名字和路径组装起来,然后 [2] open 这个文件

需要在这个 open 和前面的 chown 之间把这个文件夹创建回来,然后设置符号链接到 /etc/passwd,因为我们的最终目的是修改 passwd

下面遇到一个问题,就是 [3] ,这里用到 fstat,如果它用在符号链接的文件上,就会直接获取到符号链接到的程序的信息,而不是符号链接本身的信息,而下面有个时间判断,一般的默认情况下,passwd 创建的时间是比 boottime 要早的,这样文件就会给 unlink 掉,那这个符号链接就给删除掉

但这个操作执行一次,再第二次执行到 [3] 的时候 tries != 1,所以只要再 race 一次,创建一个符号链接就能成功建立一个符号链接指向 /etc/passwd

这也是为什么要让 chown 函数执行错误的原因

下面就到了 timestamp_lock 函数 ,这个函数里面调用 ts_write,ts_write 可以往 /etc/passwd 写入栈上的数据

bool
timestamp_lock(void *vcookie, struct passwd *pw)
{
struct ts_cookie *cookie = vcookie;
struct timestamp_entry entry;
off_t lock_pos;
ssize_t nread;
debug_decl(timestamp_lock, SUDOERS_DEBUG_AUTH)

if (cookie == NULL) {
sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
    "called with a NULL cookie!");
debug_return_bool(false);
}

/*
 * Take a lock on the "write" record (the first record in the file).
 * This will let us seek for the record or extend as needed
 * without colliding with anyone else.
 */
if (!timestamp_lock_record(cookie->fd, 0, sizeof(struct timestamp_entry)))
debug_return_bool(false);

/* Make sure the first record is of type TS_LOCKEXCL. */
memset(&entry, 0, sizeof(entry)); 
nread = read(cookie->fd, &entry, sizeof(entry));//[1]
if (nread == 0) {
/* New file, add TS_LOCKEXCL record. */
entry.version = TS_VERSION;
entry.size = sizeof(entry);
entry.type = TS_LOCKEXCL;
if (ts_write(cookie->fd, cookie->fname, &entry, -1) == -1)//[2]
    debug_return_bool(false);
} else if (entry.type != TS_LOCKEXCL) {
/* Old sudo record, convert it to TS_LOCKEXCL. */

}

static ssize_t
ts_write(int fd, const char *fname, struct timestamp_entry *entry, off_t offset)
{
ssize_t nwritten;
off_t old_eof;
debug_decl(ts_write, SUDOERS_DEBUG_AUTH)

if (offset == -1) {
old_eof = lseek(fd, 0, SEEK_CUR);
nwritten = write(fd, entry, entry->size);
} else {
old_eof = offset;

#ifdef HAVE_PWRITE
nwritten = pwrite(fd, entry, entry->size, offset);//[3]
#else
if (lseek(fd, offset, SEEK_SET) == -1) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO,
“unable to seek to %lld”, (long long)offset);
nwritten = -1;
} else {
nwritten = write(fd, entry, entry->size);
}
#endif
}
if ((size_t)nwritten != entry->size) {//[4]
if (nwritten == -1) {
log_warning(SLOG_SEND_MAIL,
N_(“unable to write to %s”), fname);
} else {
log_warningx(SLOG_SEND_MAIL,
N_(“unable to write to %s”), fname);
}

/* Truncate on partial write to be safe (assumes end of file). */
if (nwritten > 0) {
    sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
    "short write, truncating partial time stamp record");
    if (ftruncate(fd, old_eof) != 0) {
    sudo_warn(U_("unable to truncate time stamp file to %lld bytes"),
        (long long)old_eof);
    }
}
debug_return_ssize_t(-1);
}
debug_return_ssize_t(nwritten);

}
图片
上面可以看到 pwrite的执行参数,[4] 处有一个检测写入的 size 是否和传进去的 size 一样,不一样就会清空掉写入的数据,所以要在环境变量里面加 padding

图片
总结

第一种:覆盖 service_user结构体,这方法需要堆内存的碎片把握的比较好,在整个调试过程中花费了大量的时间在找一个合适的 heap 布局,使得 service_user 结构体在 user_args 的下面

第二种:就比较好调试,因为整个过程需要跑竞争,而竞争只是时间问题,使用 gdb 调试,然后一步步操作不影响后面的 exp

参考链接
https://www.qualys.com/2021/01/26/cve-2021-3156/baron-samedit-heap-based-overflow-sudo.txt)
https://github.com/stong/CVE-2021-3156)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值