0%

Linux tty 简析

tty_struct attack

当用户打开 ptmx 驱动时 open("/dev/ptmx", O_RDWR) ,会分配一个 tty_struct 结构,它的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
int index;

/* Protects ldisc changes: Lock tty not pty */
struct ld_semaphore ldisc_sem;
struct tty_ldisc *ldisc;

struct mutex atomic_write_lock;
struct mutex legacy_mutex;
struct mutex throttle_mutex;
struct rw_semaphore termios_rwsem;
struct mutex winsize_mutex;
spinlock_t ctrl_lock;
spinlock_t flow_lock;
/* Termios values are protected by the termios rwsem */
struct ktermios termios, termios_locked;
struct termiox *termiox; /* May be NULL for unsupported */
char name[64];
struct pid *pgrp; /* Protected by ctrl lock */
struct pid *session;
unsigned long flags;
int count;
struct winsize winsize; /* winsize_mutex */
unsigned long stopped:1, /* flow_lock */
flow_stopped:1,
unused:BITS_PER_LONG - 2;
int hw_stopped;
unsigned long ctrl_status:8, /* ctrl_lock */
packet:1,
unused_ctrl:BITS_PER_LONG - 9;
unsigned int receive_room; /* Bytes free for queue */
int flow_change;

struct tty_struct *link;
struct fasync_struct *fasync;
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
struct work_struct hangup_work;
void *disc_data;
void *driver_data;
spinlock_t files_lock; /* protects tty_files list */
struct list_head tty_files;

#define N_TTY_BUF_SIZE 4096

int closing;
unsigned char *write_buf;
int write_cnt;
/* If the tty has a pending do_SAK, queue it here - akpm */
struct work_struct SAK_work;
struct tty_port *port;
} __randomize_layout;

其中有一个 struct tty_operations 指针,而 tty_operations 结构体里是一些列对驱动操作的函数指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct file *filp, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
int (*get_serial)(struct tty_struct *tty, struct serial_struct *p);
int (*set_serial)(struct tty_struct *tty, struct serial_struct *p);
void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
int (*proc_show)(struct seq_file *, void *);
} __randomize_layout;

tty_struct 结构体的大小为 0x2E0,在 kernel pwn 中,如果可以有 kmalloc-1k 我们就会考虑进行 tty_struct attack(劫持 tty_operations,控制执行流)

内核的利用并不是本篇文章的重点,接下来我们将关注 tty 本身在内核中的作用

仿真终端

tty 其实是 “电传打字机(Teletypewriter)” 的缩写(后来这种设备逐渐键盘和显示器取代),泛指计算机的终端(terminal)设备

  • 在 Linux 或 UNIX 中,tty 变为了一个抽象设备(用于表示各种类型的终端设备):
    • 有时它指的是一个物理输入设备(例如串口)
    • 有时它指的是一个允许用户和系统交互的“虚拟仿真终端设备”

现代物理 IO 设备都采用“键盘+显示器”:

  • 如果用户态程序要把内容输出到显示器,只要把这些内容写入到显示器对应的 tty 设备就可以了,然后由 tty 层负责匹配合适的驱动完成输出,这也是 Linux 控制台的工作原理:
  • 显示器和键盘这类物理设备会被抽象为驱动 driver
  • 而终端仿真程序 Terminal Emulator(虚拟终端)使用驱动接口完成进一步的抽象
  • 本质上来讲键盘输入的字符是没有意义的,而终端仿真程序会对这些字符进行“格式化”,“适配”和“解释”,这个过程被称为行规程 Line Discipline
  • 经过行规程的数据将会通过 tty 和用户层进行交互,同时返回输出数据
  • 返回的数据也要经过行规程,然后被“翻译”为显示器驱动可以理解的形式并输出

在 Linux 中可以直接查看 tty 层的设备:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➜  ~ ls /dev/tty*
/dev/tty /dev/tty23 /dev/tty39 /dev/tty54 /dev/ttyS10 /dev/ttyS26
/dev/tty0 /dev/tty24 /dev/tty4 /dev/tty55 /dev/ttyS11 /dev/ttyS27
/dev/tty1 /dev/tty25 /dev/tty40 /dev/tty56 /dev/ttyS12 /dev/ttyS28
/dev/tty10 /dev/tty26 /dev/tty41 /dev/tty57 /dev/ttyS13 /dev/ttyS29
/dev/tty11 /dev/tty27 /dev/tty42 /dev/tty58 /dev/ttyS14 /dev/ttyS3
/dev/tty12 /dev/tty28 /dev/tty43 /dev/tty59 /dev/ttyS15 /dev/ttyS30
/dev/tty13 /dev/tty29 /dev/tty44 /dev/tty6 /dev/ttyS16 /dev/ttyS31
/dev/tty14 /dev/tty3 /dev/tty45 /dev/tty60 /dev/ttyS17 /dev/ttyS4
/dev/tty15 /dev/tty30 /dev/tty46 /dev/tty61 /dev/ttyS18 /dev/ttyS5
/dev/tty16 /dev/tty31 /dev/tty47 /dev/tty62 /dev/ttyS19 /dev/ttyS6
/dev/tty17 /dev/tty32 /dev/tty48 /dev/tty63 /dev/ttyS2 /dev/ttyS7
/dev/tty18 /dev/tty33 /dev/tty49 /dev/tty7 /dev/ttyS20 /dev/ttyS8
/dev/tty19 /dev/tty34 /dev/tty5 /dev/tty8 /dev/ttyS21 /dev/ttyS9
/dev/tty2 /dev/tty35 /dev/tty50 /dev/tty9 /dev/ttyS22
/dev/tty20 /dev/tty36 /dev/tty51 /dev/ttyprintk /dev/ttyS23
/dev/tty21 /dev/tty37 /dev/tty52 /dev/ttyS0 /dev/ttyS24
/dev/tty22 /dev/tty38 /dev/tty53 /dev/ttyS1 /dev/ttyS25
  • /dev/tty:(控制终端)
    • 代表当前 tty 设备
    • 在当前的终端中输入 echo hello > /dev/tty ,都会直接显示在当前的终端中
  • /dev/tty1 ~ /dev/tty6:(虚拟终端)
    • 用于表示运行在内核态的软件仿真终端
    • 可以把这些 tty 设备理解为对虚拟终端的一种抽象,使得用户程序能以操控文件的形式来与虚拟终端交互
  • /dev/tty0:(虚拟终端)
    • 代表当前虚拟终端
    • /dev/tty 主要是针对进程来说的,而 /dev/tty0 是针对整个系统来说的(所以 /dev/tty0 拥有更高的权限)
  • /dev/tty7 ~ /dev/tty63:(其他终端)
    • 用于表示运行在内核态的其他终端
    • 这些 tty 是由其他的关键软件使用的(例如 Ubuntu 中 /dev/tty7 就是图形显示管理器)
  • /dev/ttyS0 ~ /dev/ttyS31:(串行端口终端)
    • 是使用计算机串行端口连接的终端设备

我们可以做一个实验来感受一下 /dev/tty1 ~ /dev/tty6

  • 先在当前终端中输入 Ctrl + Alt + F4 切换到 /dev/tty4
  • 上图显示的就是一个虚拟终端 Terminal Emulator,用户态的 Shell 运行在它上面
  • 输入 Ctrl + Alt + F2 切换到桌面环境,输入 sudo echo "hello" > /dev/tty4,然后返回 /dev/tty4
  • 通过操作 /dev/tty4 文件,可以把用户态程序输入的内容输出到对应的虚拟终端上

可以认为 tty 是虚拟终端的一个抽象层:

  • tty 给用户态程序 Shell 提供了一种抽象,使其能够以操作文件的形式来控制各种终端

但是 tty 是运行在内核态中的,为了便于将终端仿真移入用户空间,同时仍保持 tty 子系统的完整,伪终端被发明了出来(被称为 pseudo-TTY,简称 pty

  • 每当你在系统中启动一个终端仿真器或使用任何类型的 shell 时,它都会与 pty 进行交互
  • 当创建一个伪终端时,会在 /dev/pts 目录下创建一个设备文件(用于关联 pty
1
2
3
4
5
6
➜  ~ ls -l /dev/pts 
总用量 0
crw--w---- 1 yhellow tty 136, 0 10月 7 20:24 0
crw--w---- 1 yhellow tty 136, 1 10月 7 20:24 1
crw--w---- 1 yhellow tty 136, 2 10月 7 20:41 2
c--------- 1 root root 5, 2 10月 7 19:14 ptmx
1
2
3
4
5
➜  ~ lsof /dev/ptmx
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
gnome-ter 4015 yhellow 19u CHR 5,2 0t0 87 /dev/ptmx
gnome-ter 4015 yhellow 20u CHR 5,2 0t0 87 /dev/ptmx
gnome-ter 4015 yhellow 21u CHR 5,2 0t0 87 /dev/ptmx
  • 你可以在终端仿真器中输入 tty 来找到相关联的 pty
1
2
➜  ~ tty                                        
/dev/pts/0

伪终端

伪终端(被称为 pseudo-tty,简称 pty)是指伪终端 master 和伪终端 slave 这一对字符设备,其中的 slave 对应 /dev/pts/ 目录下的一个文件,而 master 则在内存中标识为一个文件描述符(fd):

  • master 端 - ptm:是更接近用户显示器、键盘的一端(基于 VFS 的特殊文件)
  • slave 端 - pts:是在虚拟终端上运行的 CLI(Command Line Interface,命令行接口)程序

伪终端本质上是运行在用户态的终端模拟器创建的一对字符设备:

  • /dev/ptmx 是一个字符设备文件,当进程打开 /dev/ptmx 文件时,进程会同时获得:
    • 一个指向 pseudoterminal master(ptm) 的文件描述符
    • 一个在 /dev/pts 目录中创建的 pseudoterminal slave(pts) 设备
  • 建了一个伪终端对,并让 shell 运行在 slave 端:
    • 当用户在终端模拟器中按下键盘按键时,它产生字节流并写入 master 中,shell 便可从 master 中读取输入(以读文件的形式)
    • 然后 shell 和它的子程序将输出内容写入 slave 中,由 CLI 程序进行显示

可以认为 pty 是一种轻量级的虚拟终端:

  • 是一些软件(如 ssh、screen、xterm 等)模拟的 Terminal Emulator
  • 在 Linux 中右键打开的终端就是 pty

伪终端的运用

Telnet 和 SSH 都运用了伪终端技术(主要是远程登录部分):

  • 每次用户通过客户端连接服务端的时候,服务端创建一个伪终端 master、slave 字符设备对
  • 在 slave 端运行 login 程序,将 master 端的输入输出通过网络传送至客户端
  • 客户端则将从网络收到的信息直接关联到键盘/显示器上
  • 网络通信的方式还是依靠 NC 底层的 socket(TCP 协议发包)
  • 将 socket 生成的 socketFD 重定位为 master 端的“标准输入”(把管道的 stdin 重定位为 socketFD,再把管道的 stdout 重定位为 master)
  • 使 master 端和 socketFD 共用一个命名管道(命名管道相当于两个文件,一个用来读,一个用来写,它们底层使用同一个 inode 所以数据共享)
  • 最后把 login 的 stdin stdout stderr 重定位到 slave 上