ASLR 内核虚拟地址随机化

ASLR 全称 Address Space Layout Randomization,是一项 Linux 内核的安全措施,使应用程序每次加载到内存后,函数地址都不同。 试用一下 先来直观的感受下什么是 ASLR。目前大多数 linux 系统都默认开启了这个选项,可以用一下两个命令确认一下系统是否支持 ASLR。 $ cat /proc/sys/kernel/randomize_va_space 2 $ sysctl kernel.randomize_va_space kernel.randomize_va_space = 2 其中 0 表示关闭,1 表示有约束的随机,2 表示完全随机化。 然后随便找一个可执行程序,用 ldd 命令显示它加载的动态链接库,可以看到两次运行 ldd 结果各个库的地址不一样。 $ ldd /bin/sleep linux-vdso.so.1 (0x00007ffd49764000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f02783ae000) /lib64/ld-linux-x86-64.so.2 (0x00007f02789a8000) $ ldd /bin/sleep linux-vdso.so.1 (0x00007ffc10996000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f12c3534000) /lib64/ld-linux-x86-64.so.2 (0x00007f12c3b2e000) 应用程序如何使用 ASLR 在这篇文章中提到,除了 kernel 开启以外,应用程序在编译的时候也必须添加编译选项 gcc -fPIE -pie test.c 。 但是在我的实际测试中,似乎并不需要额外添加编译选项,看来 gcc 默认开启了 ASLR。 #include <stdlib.h> #include <stdio.h> void getAddr() { printf("hello, world\n"); } int main() { printf("getAddr at: %p\n", getAddr); return 0; } 或者 ...

2019-07-07 · Me

Orphan, Zombie and Docker

孤儿进程的产生 孤儿进程: 父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。通常,孤儿进程将被进程号为1的进程(进程号为 1 的是 init 进程)所收养,并由该进程调用 wait 对孤儿进程收尸。 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> int main() { pid_t pid; pid = fork(); if (pid == 0) { printf("I'm child process, pid:%d ppid:%d\n", getpid(), getppid()); sleep(5); printf("I'm child process, pid:%d ppid:%d\n", getpid(), getppid()); } else { printf("I'm father process, pid:%d ppid:%d\n", getpid(), getppid()); sleep(1); printf("father process is exited.\n"); } return 0; } 运行结果如下所示: I'm father process, pid:25354 ppid:13981 I'm child process, pid:25355 ppid:25354 father process is exited. I'm child process, pid:25355 ppid:1 一般来说,孤儿进程并没有什么危害,因为当孤儿进程结束的时候,init 进程会调用 wait 来处理。 但是当到了 Docker 环境下是不是还是这样呢? 本文第三节再详细说明。 ...

2019-06-16 · Me

Wireshark/tcpdump 抓到的数据包可信吗?

table {:toc} 问题的表面现象 问题的背景是这样的: 一个应用程序监听某端口的 UDP 包,发送者发送的 UDP 包比较大,有 65535 个字节。显然,发送者的 UDP 包在经过 IP 层时,会被拆分层多个 1460 字节长度的 IP 分片,经过网络传输之后到达接收方,接收方的网卡收到包后,在内核协议栈的 IP 层又将分片重新组合,生成大的 UDP 包给应用程序。 正常情况下,应用程序能准确无误的收到大 UDP 包,偶尔系统网络流量十分巨大的时候,会丢个别 UDP 包 ——这都在允许范围内。 突然有一天,即便网络流量不是很大的时候,UDP 包的丢包十分严重,应用程序几乎收不到任何数据包。 开始 debug 遇到这样的问题,第一反应有两种可能: 网络不通,数据包没送到网卡。 个别 IP 分片在传输中丢失了,导致接收方无法重组成完整的 UDP 包。 于是用 tcpdump 在 interface eth0 上抓包,出乎意料的是,抓到的 pcap 有完整的 IP 分片。用 wireshark 打开 pcap,wireshark 会自动把 IP 分片重组成 UDP 数据包,检查这个 UDP 包,数据完整无误。 那么现在能断定是应用程序自己出了问题吗? 因为一直以来一个根深蒂固的想法是:既然抓到的 pcap 准确无误,所以数据包已经送到了接收方了,linux 内核只要把分片重组一下交给应用层就可以了,这个过程一般不会出错,所以应用程序没收到只能怪它自己咯? 实际导致问题的原因 最终查明的原因是这台 linux 系统上有两个参数被修改了,当 IP 分片数量过大时,内核中分配给重组的缓冲区已满,导致之后的分片都被丢弃了。 ...

2018-10-24 · Me

Linux 共享内存以及 nginx 中的实现

共享内存方法简介 Linux/Unix系统中,共享内存可以通过两个系统调用来获得,mmap 和 shmget/shm_open,其中 shmget 和 shm_open 分别属于不同的标准: POSIX 共享内存(shm_open()、shm_unlink()) System V 共享内存(shmget()、shmat()、shmdt()) shmget 和 shm_open 类似的地方在于都是创建一个共享内存,挂载到 /dev/shm 目录下,并且返回一个文件描述符,fd。 区别是 POSIX 没有提供将 fd 映射到进程地址空间的方法,而 System V 方式则直接提供了 shmat(),之后再 nginx 的实现中会再次看到。 mmap 语义上比 shmget 更通用,因为它最一般的做法,是将一个打开的实体文件,映射到一段连续的内存中,各个进程可以根据各自的权限对该段内存进行相应的读写操作,其他进程则可以看到其他进程写入的结果。 而 shmget 在语义上相当于是匿名的 mmap,即不关注实体文件,直接在内存中开辟这块共享区域,mmap 通过设置调用时的参数,也可达到这种效果,一种方法是映射/dev/zero 设备,另一种是使用MAP_ANON选项。 mmap() 的函数原型如下,具体参数含义在最后的参考资料中给出。 void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset); nginx 中的实现 nginx 中是怎么实现的呢? 我们看一下源码 src/os/unix/ngx_shmem.c。 一目了然,简单粗暴有木有! 分三种情况 如果mmap系统调用支持 MAP_ANON选项,则使用 MAP_ANON 如果1不满足,如果mmap系统调用支持映射/dev/zero设备,则映射/dev/zero来实现。 如果1和2都不满足,且如果支持shmget的话,则使用该shmget来实现。 #if (NGX_HAVE_MAP_ANON) ngx_int_t ngx_shm_alloc(ngx_shm_t *shm) { shm->addr = (u_char *) mmap(NULL, shm->size, PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0); return NGX_OK; } #elif (NGX_HAVE_MAP_DEVZERO) ngx_int_t ngx_shm_alloc(ngx_shm_t *shm) { fd = open("/dev/zero", O_RDWR); shm->addr = (u_char *) mmap(NULL, shm->size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); return (shm->addr == MAP_FAILED) ? NGX_ERROR : NGX_OK; } #elif (NGX_HAVE_SYSVSHM) ngx_int_t ngx_shm_alloc(ngx_shm_t *shm) { id = shmget(IPC_PRIVATE, shm->size, (SHM_R|SHM_W|IPC_CREAT)); shm->addr = shmat(id, NULL, 0); if (shmctl(id, IPC_RMID, NULL) == -1) { } return (shm->addr == (void *) -1) ? NGX_ERROR : NGX_OK; } 上面代码中的宏定义(比如 NGX_HAVE_MAP_ANON )是怎么来的呢?编译 nginx 源码之前的一步是运行 configure 文件,它会调用 auto/unix 脚本 ,该脚本会写一端测试程序来判断相应的系统调用是否支持,如果支持,则在自动生成的 objs/ngx_auto_config.h 文件中定义对应的宏。 ...

2018-09-04 · Me

使用 socketpair 实现进程间通信

socketpair 牛刀小试 int socketpair(int d, int type, int protocol, int sv[2]); 第1个参数d,表示协议族,只能为 AF_LOCAL 或者 AF_UNIX; 第2个参数 type,表示类型,只能为0。 第3个参数 protocol,表示协议,可以是 SOCK_STREAM 或者 SOCK_DGRAM AF_UNIX 指的就是 Unix Domain socket,那么它与通常网络编程里面的 TCP socket 有什么区别呢? 查阅了资料后发现: Unix Domain socket 是同一台机器上不同进程间的通信机制。 IP(TCP/IP) socket 是网络上不同主机之间进程的通讯机制。 socketpair() 只支持 AF_LOCAL 或者 AF_UNIX,不支持 TCP/IP,也就是 AF_INET, 所以用 socketpair() 的话无法进行跨主机的进程间通信。 先看一个简单的示例: int main() { int fd[2], retpid; int pid , status; char input[MAX_LEN]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0) { printf("call socketpair() failed, exit\n"); return -1; } pid = fork(); if (pid) { /* parent */ printf("Parent process, pid = %d\n", getpid()); while (1) { fgets(input, MAX_LEN, stdin); write(fd[0], input, MAX_LEN); } } else { /* child */ printf("Child process, pid = %d\n", getpid()); int nread = 0; while (1) { nread = read(fd[1], input, MAX_LEN); input[nread] = '\0'; printf("Child: nread = %d, data = %s\n", nread, input); } } retpid = wait(&status); if (retpid) { printf("Parent: reap child process pid = %d\n", retpid); } return 0; } 编译后运行,可以看到每次在终端输入信息,子进程都会回显到屏幕上。 ...

2018-08-28 · Me

BPF -- Linux 中的 DTrace

记得 5 年前刚接触 perf 的时候,还特意调研了一下不同系统上的动态和静态追踪工具,知道了 Linux 上的 SystemTap,perf。Solaris 上的 DTrace。看到绝大多数资料都说 DTrace 多么的强大好用,但是 Linux 却没有与之相提并论的工具。 最近看到 BPF 这三个字被提及的很频繁,搜索了一下发现它号称 “Linux 中的 DTrace”, 于是试着玩了一下。 BPF 全称是 “Berkeley Packet Filter”,字面意思是包过滤器,那么问题来了:我一个包过滤器,怎么就成了追踪调试工具呢? 这主要是因为一些历史的进程:原先开发 BPF 的目的是在内核重定向数据包,接着增加了对事件的追踪功能,然后又增加了基于时间的采样,于是久而久之 BPF 就成了一个功能强大的调试工具。 安装 首先,内核版本最好大于 4.9 , 可以用 uname -a 命令查看。 其次,查看一下内核在编译的时候是否开启了 BPF 选项,一般在 /boot/ 目录下有对应内核版本的 config 文件,比如在我的机器上是 /boot/config-4.15.0-30-generic。 如果看到 CONFIG_BPF_SYSCALL=y 说明可以用 BPF 的基本功能。 前面提到 BPF 号称 Linux 中的 DTrace,为什么呢? 因为 DTrace 包含了一个类似脚本语言的 D 语言,用户可以用简单的几句 D 语言完成复杂的调试追踪任务,这一点是 perf 做不到,而 BPF 做到了。 确认了内核支持 BPF 之后,我们可以安装一个叫做 bcc 的工具,通过它可以方便的使用 BPF。 ...

2018-08-09 · Me

SO_REUSEPORT 和 epoll 的 Thundering Herd

SO_REUSEPORT 顾名思义就是重用端口,是指不同的 socket 可以 bind 到同一个端口上。 Linux 内核 3.9 版本引入了这个新特性,有兴趣的同学可以移步到这个链接查看更加详细的内容。 https://lwn.net/Articles/542629/ Reuse Port 我们先通过一段简单的代码来看看怎么使用这个选项(完整的代码在这里下载)。 int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 一定要在 bind() 函数之前设定好 SO_REUSEPORT setsockopt(serv_sock, SOL_SOCKET, SO_REUSEPORT, &enable, sizeof(int)); bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); listen(serv_sock, 20); accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); 将上面的代码编译生成两个可执行文件,分别启动运行,并监听相同的端口。 ./port_reuse1 127.0.0.1 1234 再用 telnet/nc 等工具发送请求到 1234 端口上,多重复几次,会看到两个进程轮流的处理客户端发来的请求。 这里说一个题外话,上面的例子是手动启动两个进程。而我发现如果是进程自动 fork() 生成 2 个进程的话,似乎不用设置 SO_REUSEPORT 也能自动监听同一个端口。这是为什么? Thundering Herd / 惊群现象 The thundering herd problem occurs when a large number of processes waiting for an event are awoken when that event occurs, but only one process is able to proceed at a time. After the processes wake up, they all demand the resource and a decision must be made as to which process can continue. After the decision is made, the remaining processes are put back to sleep, only to all wake up again to request access to the resource. ...

2018-06-23 · Me

如何拦截库函数调用 ?

LD_PRELOAD 环境变量 : 直接作用在可执行文件上 (准确的说是拦截库函数) 。 ptrace() : 拦截子进程的系统调用。 1. LD_PRELOAD LD_PRELOAD 的优势: 使用简单。 不需要修改被拦截程序的源码。 例如我想拦截程序 A 所有调用 malloc() 的地方,那么程序 A 不需要任何修改,只要准备好自己的 malloc() 函数,编译成动态链接库 .so 文件,然后在运行 A 之前先用 LD_PRELOAD 设定好环境变量就可以了。 LD_PRELOAD 的原理就是链接器在动态链接的时刻,优先链接 LD_PRELOAD 指定的函数。准确的说 LD_PRELOAD 拦截的是动态库中的函数,但是一般我们写的应用程序都是通过库函数来调用系统调用 API,所以 LD_PRELOAD 也间接的拦截了系统调用。 说到这里,LD_PRELOAD 的缺点也非常明显,它只能作用于动态链接库,要是静态链接的就没戏了。 腾讯的 C++ 协程库 libco,以及 tcmalloc 的 TC_MALLOC 都用到了这种方式。 2. ptrace() ptrace 是 linux 内核原生提供的一个功能,因此功能比 LD_PRELOAD 强大的多。它最初的目的是用来 debug,例如大名鼎鼎的 gdb 就是依赖于 ptrace。 要使用 ptrace 拦截程序 A 的系统调用,有两种方法: ptrace 一个新进程:在代码中 fork 一个子进程,子进程执行 ptrace(PTRACE_TRACEME, 0, 0, 0)函数,然后通过 execv() 调用程序 A。 attach 到已运行的程序 A :执行ptrace(PTRACE_ATTACH, pid, 0, 0)。 以上两种方式,ptrace 都会拦截发送到 A 进程的所有信号(除 SIGKILL 外),然后我们需要自己选择哪些系统调用需要拦截,并在拦截后转到我们自己的处理函数。 ...

2018-06-03 · Me

TCP 的 FIN_WAIT1 状态

最近看到了一篇有关 TCP 关闭连接时 FIN-WAIT1 状态的文章(见参考资料),觉得很有意思,于是也在自己的电脑上验证了一下。 首先,开局一张图: {:height=“300” width=“300”} FIN-WAIT1 状态出现在 主动关闭链接方发出 FIN 报文后,收到对应 ACK 之前。通常 Server 在收到 FIN 报文之后,会在很短的时间内回复 ACK(这个 ACK 可能携带数据,也可能只是一个纯 ACK),所以 FIN-WAIT1 状态存在的时间非常短暂,很难被观察到。 于是准备两台虚拟机,我们可以设计这样一个实验: 1)服务端监听 1234 端口:nc -l 1234 2)客户端连接服务端:nc 192.168.122.183 1234, 此时 TCP 的状态为 ESTABLISHED $ sudo netstat -anp | grep tcp tcp 0 0 192.168.122.167:60482 192.168.122.183:1234 ESTABLISHED 3712/nc 3)服务端配置 iptables,拦截从服务端发送到客户端的任何报文:iptables -A OUTPUT -d 192.168.122.167 -j DROP 4)客户端按下 ctrl + c 断开连接,这一步的目的是让操作系统自动发送 FIN 报文给服务端。 在完成第 4 步之后,客户端就会进入 FIN-WAIT-1 状态,服务端也会收到 FIN 报文,并且马上会发出一个 ACK,但是因为配置了 iptables,因此客户端会一直等待服务端的 ACK。 ...

2017-08-09 · Me

Linux 内核在 x86-64 上的内存分区

如果稍微了解过 Linux 内核的内存管理,那么对内存分区的概念一定不陌生,Linux内核把物理内存分成了3个区, 0 – 16M 为ZONE_DMA区, 16M – 896M 为ZONE_NORMAL区, 高于896M 为ZONE_HIGHMEM区 我没有去考证过为什么要取896这个数字,但是可以肯定的是这样的划分在当时看来是合理的,然而计算机发展今非昔比,现在4G的物理内存已经成为PC的标配了,CPU也进入了64位时代,很多事情都发生着改变。 在CPU还是32位的时代,CPU最大的物理寻址范围是0-4G, 在这里为了方便讨论,我们不考虑物理地址扩展(PAE)。进程的虚拟地址空间也是 4G,Linux内核把 0-3G虚拟地址空间作为用户空间,3G-4G虚拟地址空间作为内核空间。 目前几乎所有介绍Linux内存管理的书籍还是停留在32位寻址的时代,所以大家对下面这张图一定很熟悉! (这个图画得非常详细,本篇文章我们关注的重点是 3个分区 以及最右边的线性地址空间,也就是虚拟地址空间之间的关系,另外,应该是ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM, 图中把ZONE 写成了ZUNE) 然而,现在是64位的时代了, 64位CPU的寻址空间是多大呢? 16EB, 1EB = 1024 TB = 1024 * 1024 GB,我想很多人这辈子还没见过大于1TB的内存吧,事实上也是这样,几乎没有哪个服务器能有16EB的内存,实现64位长的地址只会增加系统的复杂度和地址转换的成本,所以目前的x86_64架构CPU都遵循AMD的 Canonical Form, 即只有虚拟地址的最低48位才会在地址转换时被使用, 且任何虚拟地址的48位至63位必须与47位一致, 也就是说总的虚拟地址空间为256TB。 那么在64位架构下,如何分配虚拟地址空间的呢? 0000000000000000 – 00007fffffffffff(128TB)为用户空间, ffff800000000000 – ffffffffffffffff(128TB)为内核空间。 而且内核空间中有很多空洞, 越过第一个空洞后, ffff880000000000 – ffffc7ffffffffff(64TB) 才是直接映射物理内存的区域, 也就是说默认的PAGE_OFFSET为 ffff880000000000. 请关注下图的最左边,这就是目前64位的虚拟地址布局。 在本文的一开头提到的物理内存分区 ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM 就是与内核虚拟地址的直接映射有关的,如果读者不了解 内核直接映射物理地址这个概念的话,建议你去google一下,这个很简单的一一映射的概念。 既然现在内核直接映射的物理内存区域有64TB, 而且一般情况下,极少有计算机的内存能达到64TB(别说64TB了,1TB内存的也很少很少),所以整个内核虚拟地址空间都能够一一映射到计算机的物理内存上,因此,不再需要 ZONE_HIGHMEM这个分区了,现在对物理内存的划分,只有ZONE_DMA, ZONE_NORMAL。 ...

2014-01-02 · Me