Redis 的作者 Salvatore Sanfilippo(网名 antirez)在意大利西西里岛长大,虽然从小就接触计算机,也有一些编程经验,但在大学期间却选择了建筑学院,可能当时并没有打算走职业程序员的道路吧。
然而 antirez 应该就属于老天爷赏饭的那类人,据说仅仅因为错把显卡买成了网卡,商家又不肯退货,他就放下游戏,拿起了 C 语言的教材。不久之后,antirez 发现了一个 ping 的漏洞,非 root 用户也可以执行 flood ping。这是怎么回事呢?
ping 命令普通的工作方式是间隔 1 秒就发送 1 个 ping 数据包,而这种周期性是借助系统的定时器所产生的 SIGALRM
信号实现的。超时时间为 1 秒的定时器一到期,就会产生 SIGALRM
信号,进而触发 ping 数据包的发送。
既然只要收到 SIGALRM
信号就会发送 ping 数据包,那如果绕过定时器,大量伪造这种信号呢?antirez 发现 ping 根本不校验这个信号是否真的是因定时器到期(超时)而发出的。于是,antirez 利用一些编程技巧,不需要 root 权限就可以疯狂产生 SIGALRM
信号,间接达到了 flood ping 的效果。
1998 年 5 月,21 岁的 antirez 将这一发现发布到一个专注于计算机安全漏洞披露的公共邮件列表上,并很快得到了网络安全公司 SECLAB 的垂青,一通来自米兰的长途电话邀请他前往 SECLAB 工作。
下面简单分析一下学生时代(21 岁)的 antirez 在 https://seclists.org/bugtraq/1998/May/139 上提交的代码,看看他是如何利用 ping 的漏洞,绕过了“只有 root 账户才能执行 flood ping”的限制,让普通用户账户也能一刻不停疯狂地向目标主机发送 ICMP 报文。
#include <signal.h>
#define PING "/bin/ping"
main( int argc, char *argv[] )
{
int pid_ping;
// ...
if(!(pid_ping = fork())) // ①
execl(PING, "ping", argv[1], NULL); // ②
if ( pid_ping <=0 ) {
// 处理 fork() 的错误
}
sleep (1); /* give it a second to start going */
while (1) // ③
if ( kill(pid_ping, SIGALRM) ) // ④
// 处理 kill() 的错误
}
首先,调用 fork()
系统调用创建一个子进程①。如果子进程创建成功,那么在父进程中,变量 pid_ping
中存放的是子进程的进程 id(大于 0);而在子进程中,pid_ping
的值为 0。
当 pid_ping == 0
时,即在子进程中,if
的条件 (!pid_ping)
成立。此时通过调用 execl()
让子进程执行 ping 命令②,向通过参数 argv[1]
指定的目标主机发送 ICMP 报文,就像直接在命令行中执行ping <hostname>
一样。
ping 命令向目标主机发送 ICMP Echo Request 报文 是通过定时器和 SIGALRM
信号驱动的。ping 只要收到 SIGALRM
信号就会发送一个 ICMP Echo Request 报文,发送完后随即通过 alarm(1)
设定定时器在 1 秒后重新产生 SIGALRM
信号,从而能够继续收到该信号,继续发送下一个 ICMP 报文。直到用户按下 Ctrl + C 退出程序或已发送了指定数量的 ICMP 报文。
这套机制看似能够每间隔 1 秒就发送 1 个 ICMP Echo Request 报文,但 antirez 敏锐地发现了其中的漏洞。既然 ping 只要收到 SIGALRM
信号就会发送 ICMP 报文,那要是伪造大量这种信号呢?ping 会不会根本不校验这个信号是否真的是因定时器到期(超时)而发出的?于是,antirez 在父进程中写了个死循环③,疯狂地调用 kill()
向子进程(进程 id 存放在 pid_ping
中)发送 SIGALRM
信号④。最终达到了不需要 root 权限就可以疯狂产生 SIGALRM
信号,间接实现 flood ping 的效果。
以上便是21 岁还在念书的 antirez 的发现。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。