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。...

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....

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) ?...

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....

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。 如果你想有个更加直观的了解的话,请打开 /boot/config*** 文件,例如我的是 /boot/config-3....

2014-01-02 · Me