信息来源:GSU
目的很简单:快速扫描多个B类或C类网络里的RPC服务漏洞. 如果只为扫描几台服务器,那么大概30行左右的代码就可以. 在几十万台主机的情况下,速度是一个必需优先考虑的问题. 所以首先,我们的程序必需是多线程的,并且如果可能,就用空间换时间.
现在可以写出程序的框架:
int main(int argc,char **argv)
{
init_scan();
do_scan();
end_scan();
return 0;
}
init_scan读入配置信息,主要是要扫描的网络列表,也就是完成输入工作;
do_scan做线程调度,完成实际的扫描工作; 而end_scan输出扫描结果. 我把程序程序理解为输入,输出,数据结构,再加算法.
现在该数据结构定义了. 显然需要一个描述主机及其RPC漏洞信息的数据对象.
下面的定义简单并且直观:
typedef struct _IPENTRY
{
/* use in_addr_t will slow scan_speed,but use few momory */
char host[HOSTLEN];
unsigned long vuls;
} IPENTRY;
static IPENTRY iplists[MAXHOSTS];
起先我用in_addr_t来描述IP地址,因为这可以节约存储空间;然而,RPC库函数却要求参数必需是char *host,这意味着如果定义为In_addr_t,那么每次扫描必需有一个格式
转换操作,几十万次转换的开销肯定不会小. 因此定义为char host[16].vuls 的每一位代表一种漏洞. 在SPARC V9上,最多可以有64位.
另一个需要的数据结构是RPC漏洞信息的描述. 譬如program,version等.
typedef struct _RPCSVC
{
rpcprog_t prog;
rpcvers_t ver;
rpcprot_t prot;
char *desc;
int (*exploit)(const char*);
} RPCSVC;
static RPCSVC rpcsvcs[] = { ......... };
现在,定义完啦必需的数据结构.可以更清楚地描述init_scan,do_scan和end_scan的工作啦. init_scan就是把直观的地址信息映射如IPENTRY结构,而do_scan对每个IPENTRY的vuls成员置位,end_scan以各种格式输出IPENTRY结构. 是不是有点类似MFC的文档与视结构?
do_scan是这个扫描程序的核心. 显然它要create许多线程来扫描. 问题是,如何在这些线程间分配IPENTRY?
直观的办法是把iplists按线程数目等分. 然而这会造成一些工作比较快的线程扫描完后无所事事; 不过优点时没有同步开销. 为啦简单和避免线程空闲,采用每个线程不断从iplists中抢夺未扫描主机的办法,一旦抢到,线程就埋头扫描,扫描完后再抢; 如果因为同步抢不到,线程就暂时等待.
线程函数主体如下:
while(1) {
pthread_mutex_lock(&mutex_curpos);
mypos = curpos;
if (mypos >= totnum) {
pthread_mutex_unlock(&mutex_curpos);
break;
}
curpos++;
pthread_mutex_unlock(&mutex_curpos);
for (i=0; i<NUMRPC; i++){
扫描每种漏洞
}
}
mutex_lock是必需付出的同步开销.
现在,可以把do_scan用如下代码段替换:
for (i=0; i<thread_num; i++) {
pthread_create(&tids,NULL,scan,NULL);
}
scan就是前面描述的线程函数,它从全局的iplists中取数据,因而不需要参数.
哪么,main如何知道所有的线程已经做完工作? 不能简单根据curpos(已扫描主机数)来判断. 因为线程是先增加curpos,再做扫描的. 所以有可能当curpos等于读入主机数后仍有许多线程在工作. 第一种解决办法是pthread_join:
for (i=0; i<thread_num; i++) {
pthread_join(tids,NULL);
}
这样做的缺点是:加入线程0最后终止,那么我们的phread_join就只能一直阻塞在线程0,无法去处理其它已经终止的线程. 而且,我们根本就不关心各个线程的推出状态,为什么要等待它们呢? 设置线程属性为detached是一个自然的选择.
为了判断所有线程已经终止而又不用pthread_join,只好又引入另外一个pthread同步对象:条件变量.
/* current working threads number */
static int curnum = 0;
/* mutex lock for curnum */
static pthread_mutex_t mutex_curnum = PTHREAD_MUTEX_INITIALIZER;
/* cond for curnum */
static pthread_cond_t cond_curnum = PTHREAD_COND_INITIALIZER;
变量curnum代表当前活动线程数,在线程scan函数结尾增加一下代码:
pthread_mutex_lock(&mutex_curnum);
if (!--curnum)
pthread_cond_signal(&cond_curnum);
pthread_mutex_unlock(&mutex_curnum);
return NULL;
每个线程终止时把curnum减1,如果curnum等0,那么通知main所有线程都已终止. main在创建完线程后就已以下代码阻塞在cond_curnum上:
pthread_mutex_lock(&mutex_curnum);
while(curnum)
pthread_cond_wait(&cond_curnum,&mutex_curnum);
pthread_mutex_unlock(&mutex_curnum);
现在回到scan函数. scan是线程的工作函数,它决定着整个扫描的速度.
scan经过三次修改,但是都集中在for (i=0; i<NUMRPC; i++)循环里.
最初for循环里的代码如下:
client = clnt_create(iplists[mypos].host,rpcsvcs.prog,rpcsvcs.ver,"tcp");
if (client || clnt_create(........,"udp")) {
iplists[mypos].vuls |= (1<<i);
clnt_destroy(client);
}
这段代码里还可以看出小四的RPC/XDR系列的影子. :)
然后就是加入超时处理的代码. 不像Linux,Solaris有现成的函数:
clnt_create_timed. 所以不需要加入复杂的longjmp代码,只用把clnt_create换成clnt_create_timed,最后再加入一个timeval参数就是我们的超时版本.
这时候程序的效率如下: 200个线程,超时为1秒,扫描内部10M局域网时大约是9 台主机/每秒. 这个速度太慢了. 必需考虑进一步修改.
首先我注意到RPC的一个错误码:RPC_PROGNOTREGISTERED. 如果tcp的clnt_create失败而错误码不是RPC_PROGNOTREGISTERED,那么再做udp的clnt_create基本上没有
意义:它必然失败. 因此if语句可以修改如下:
if (client || (rpc_createerr.cf_stat == RPC_PROGNOTREGISTERED &&
(client=clnt_create_timed(........,"udp",&tv)))
只有在错误码为RPC_PROGNOTREGISTERED时才做udp的clnt_create.
这样修改后,速度提高到 19 台主机/每秒.
一天后我又考虑到另外一个错误码:RPC_RPCBFAILURE. 思路时:能否减少for循环的次数? 理由是:对一台不存在的主机或没有RPC服务的主机,为什么要把所有的RPC漏洞都尝试一遍? 如果能在第一次clnt_create失败后即判断出该IP根本没有RPC服务,那么我就可以立刻跳出for循环. 我在clnt_create_timed(tcp)和if语句间又加入:
if (rpc_createerr.cf_stat == RPC_RPCBFAILURE) {
break;
}
如果失败原因是portmapper导致的,那么就应该立刻终止循环.
考虑到绝大多数IP都没有RPC服务,我把udp的clnt_create放在开始,把tcp的放入if语句里. 因为udp更快,因而在通常情况下,扫描会更快.
然后clnt_create_timed函数再次被另一个函数替换: clnt_tp_create_timed.这是为拉取消对"tcp","udp"参数的处理. man getnetconfigent 你就会明白为什么这么处理.
现在速度提高为 84 台主机/每秒. 就是说 3 秒钟扫描完局域网. 但是,现在不肯能再用数字衡量速度啦. 主机数越多,打开RPC服务的主机越少,每秒扫描的主机数就会越多. 测试中的最高数字是 288 IP/S.
多少个线程最合适? 这个问题理论上没法回答. :(
在我用的Ultra 5 (Solaris 8)下,试验结果时超过500个线程速度反而下降. 最后我把缺省线程数定为 400. 当然,如果要扫描的主机数少于 400,实际线程数就被设为主机数.
另外一个重要参数是超时时间. 理论上超时设的越长扫描结果应该越准确,实际中恰恰相反. 例如,设为 1 秒可以扫描出 4台主机,设为 2 秒只能扫出三台,6秒就只能扫出两台. 这个结果严重违背直观想象,原因不明.
clnt_create函数族其实只是RPC端口扫描. 它并不说明RPC服务真的存在并且有漏洞. 因此我把扫描细分为一次扫描和二次扫描. 一次扫描从几十万个IP中筛选出有RPC服务的IP(譬如有snmpXdmid),然后二次扫描再用多线程扫描有snmpXdmid的IP,做更准确的鉴定,如溢出测试,这是由RPCSVC结构中的exploit函数指针完成.
这是二次扫描的线程函数:
static void *stage2_scan(void *arg)
{
register int i;
IPENTRY *ie = (IPENTRY*)arg;
register long vuls = ie->vuls;
#ifdef DEBUG
fprintf(stderr,"Stage2_scan: %s : %lx\n",ie->host,ie->vuls);
#endif
for (i=0; vuls; i++,vuls >>=1) {
/* Is it exploitable? */
if ( (vuls & 1) && rpcsvcs.exploit && !rpcsvcs.exploit(ie->host) ) {
ie->vuls &= (~(1<<i));
}
}
pthread_mutex_lock(&mutex_curnum);
if (!--curnum)
pthread_cond_signal(&cond_curnum);
pthread_mutex_unlock(&mutex_curnum);
return NULL;
}
main 中的初始化工作:
/*
* In stage1_scan,we doesn't check wether tcp or udp is NULL for
* speed reason.
*/
if ((tcp=getnetconfigent("tcp")) == NULL)
fprintf(stderr,"Transport protocol TCP isn't available,disable.\n");
if ((udp=getnetconfigent("udp")) == NULL)
fprintf(stderr,"Transport protocol UDP isn't available,disable.\n");
if (!tcp && !udp) {
fprintf(stderr,"Both UDP and TCP aren't available,/etc/netconfig error?\n");
exit(-1);
}
fprintf(stderr,"Scan init %s...\n",flag?"(continue) ":"");
init_scan(flag);
/* Set highest priority */
if (!geteuid()) {
fprintf(stderr,"Set highest priority \n");
setpriority(PRIO_PROCESS,0,-20);
}
/* Raise number of open files */
if (!getrlimit(RLIMIT_NOFILE,&rl)) {
fprintf(stderr,"Set Max open files to : %d\n",rl.rlim_max);
rl.rlim_cur = rl.rlim_max;
setrlimit(RLIMIT_NOFILE,&rl);
}
if (thdnum + 10 > rl.rlim_cur) {
thdnum = rl.rlim_cur - 10;
fprintf(stderr,"Too many threads,set threads number to %d\n",thdnum);
}
if (totnum - prevpos < thdnum)
thdnum = totnum - prevpos;
pthread_attr_init(&attr);
pthread_attr_setscope(&attr,PTHREAD_SCOPE_SYSTEM);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
/* Open save-file,for we may can't open it in save_data later */
if ((fsave=fopen(FSAVE,"wb")) == NULL) {
fprintf(stderr,"Can't open file %s for data save. disable save feature\n",FSAVE);
}
memset(&sa,0,sizeof sa);
sa.sa_handler = sig_handler;
sa.sa_flags |= SA_SIGINFO;
sigaction(SIGINT,&sa,NULL);
sigaction(SIGHUP,&sa,NULL);
sigaction(SIGABRT,&sa,NULL);
sigaction(SIGTERM,&sa,NULL);
sigaction(SIGALRM,&sa,NULL);
/*
* ITIMER_PROF won't work in MT application under Solaris 8. But,
* "New MT applications should not use this flag(ITIMER_REAL),
* and should use alarm(2) instead." ---Solaris 8 manual
*/
setitimer(ITIMER_REAL,&it,NULL);
进一步的修改.
再回到IPENTRY结构. 考虑扫描一个A类网络的情况,iplists将会占用几十兆内存. 而且,因为clnt_create族函数第一个参数为cosnt char *host这意味着它们必然要对host做解析. 但是因为输入是IP地址,这个解析是根本不需要的. 所以,从时间和空间上考虑,host都应该被定义为in_addr_t类型. Solaris 8 TI-RPC的源码早就公布了,所以可以直接那源码改出一个接受in_addr_t参数的clnr_create_***.
还有IPENTRY的vuls. 毕竟极少数IPENTRY用到啦这个vuls,其它绝大多数都没有用到. 因此,为每个ip指定一个vuls只是浪费空间.